depot3 0.0.0a1 → 3.0.7

Sign up to get free protection for your applications and to get access to all the features.
Files changed (112) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +55 -1
  3. data/bin/d3 +323 -0
  4. data/bin/d3admin +1011 -0
  5. data/bin/d3helper +354 -0
  6. data/bin/puppytime +334 -0
  7. data/data/d3/com.pixar.d3.RepoMan.plist +23 -0
  8. data/data/d3/d3.conf.example +507 -0
  9. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftAppKit.dylib +0 -0
  10. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCore.dylib +0 -0
  11. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreData.dylib +0 -0
  12. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreGraphics.dylib +0 -0
  13. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftCoreImage.dylib +0 -0
  14. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftDarwin.dylib +0 -0
  15. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftDispatch.dylib +0 -0
  16. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftFoundation.dylib +0 -0
  17. data/data/d3/d3RepoMan.app/Contents/Frameworks/libswiftObjectiveC.dylib +0 -0
  18. data/data/d3/d3RepoMan.app/Contents/Info.plist +56 -0
  19. data/data/d3/d3RepoMan.app/Contents/MacOS/d3RepoMan +0 -0
  20. data/data/d3/d3RepoMan.app/Contents/PkgInfo +1 -0
  21. data/data/d3/d3RepoMan.app/Contents/Resources/Base.lproj/MainMenu.nib +0 -0
  22. data/data/d3/d3RepoMan.app/Contents/Resources/last-foreground-times-template.plist +5 -0
  23. data/data/d3/d3RepoMan.app/Contents/_CodeSignature/CodeResources +214 -0
  24. data/data/d3/puppytime/ImageLicenses.txt +165 -0
  25. data/data/d3/puppytime/notification_image +1 -0
  26. data/data/d3/puppytime/opt_out_image +1 -0
  27. data/data/d3/puppytime/slideshow/2008-07-11_White_German_Shepherd_pup_chilling_at_the_Coker_Arboretum.jpg +0 -0
  28. data/data/d3/puppytime/slideshow/2009-04-21_APBT_pup_on_deck.jpg +0 -0
  29. data/data/d3/puppytime/slideshow/A_puppy_Yorkie.jpg +0 -0
  30. data/data/d3/puppytime/slideshow/Alert_Pug_Puppy.jpg +0 -0
  31. data/data/d3/puppytime/slideshow/Australian_Cattle_Dog_puppies_04.JPG +0 -0
  32. data/data/d3/puppytime/slideshow/Beagle_puppy_Cadet.jpg +0 -0
  33. data/data/d3/puppytime/slideshow/Bernese_Mountain_Dog.jpg +0 -0
  34. data/data/d3/puppytime/slideshow/Bloodhound_Puppy.jpg +0 -0
  35. data/data/d3/puppytime/slideshow/Boston_terrier_with_toy.jpg +0 -0
  36. data/data/d3/puppytime/slideshow/Boxer_puppy_fawn_portrai.jpg +0 -0
  37. data/data/d3/puppytime/slideshow/Caracal_kitten.jpg +0 -0
  38. data/data/d3/puppytime/slideshow/Chihuahua_&_Doberman_Pup.jpg +0 -0
  39. data/data/d3/puppytime/slideshow/Cuccioli_di_Margot_a_35_gg_Basenjis.jpg +0 -0
  40. data/data/d3/puppytime/slideshow/Dalmatian_puppy_03.jpg +0 -0
  41. data/data/d3/puppytime/slideshow/GoldenRetrieverPuppyDaisyParker.JPG +0 -0
  42. data/data/d3/puppytime/slideshow/Green_eyed_beige_Chihuahua.jpg +0 -0
  43. data/data/d3/puppytime/slideshow/Let_Sleeping_Dogs_Lie.jpg +0 -0
  44. data/data/d3/puppytime/slideshow/Meatball_-_French_Bulldog_Puppy.jpg +0 -0
  45. data/data/d3/puppytime/slideshow/Oola_-_9_weeks.jpg +0 -0
  46. data/data/d3/puppytime/slideshow/Pancho0008.JPG +0 -0
  47. data/data/d3/puppytime/slideshow/Pomeranian_orange-sable_Coco.jpg +0 -0
  48. data/data/d3/puppytime/slideshow/Pug_puppy_001.jpg +0 -0
  49. data/data/d3/puppytime/slideshow/Puggle_puppy_6_weeks.JPG +0 -0
  50. data/data/d3/puppytime/slideshow/Puli_kan.jpg +0 -0
  51. data/data/d3/puppytime/slideshow/Puppy_French_Bulldog.jpg +0 -0
  52. data/data/d3/puppytime/slideshow/Rocco_the_Bulldog.jpg +0 -0
  53. data/data/d3/puppytime/slideshow/Rottweiler_Face.jpg +0 -0
  54. data/data/d3/puppytime/slideshow/Saint_Bernard_puppy.jpg +0 -0
  55. data/data/d3/puppytime/slideshow/Scottish_froment.jpg +0 -0
  56. data/data/d3/puppytime/slideshow/Shar_pei_puppy_(age_2_months).jpg +0 -0
  57. data/data/d3/puppytime/slideshow/Shiba-Inu_beim_Spielen_im_Schnee.JPG +0 -0
  58. data/data/d3/puppytime/slideshow/Smooth-coat_Border_Collie_puppy..jpg +0 -0
  59. data/data/d3/puppytime/slideshow/Smooth_Dachshund_puppies.jpg +0 -0
  60. data/data/d3/puppytime/slideshow/Snow_dog.jpg +0 -0
  61. data/data/d3/puppytime/slideshow/Taylor_the_Pembroke_Welsh_Corgi.png +0 -0
  62. data/data/d3/puppytime/slideshow/Weim_Pups_001.jpg +0 -0
  63. data/data/d3/puppytime/slideshow/Westie_pups.jpg +0 -0
  64. data/data/d3/puppytime/slideshow/Yellow_Labrador_puppies_(4165737325).jpg +0 -0
  65. data/lib/d3/admin/add.rb +451 -0
  66. data/lib/d3/admin/auth.rb +470 -0
  67. data/lib/d3/admin/edit.rb +297 -0
  68. data/lib/d3/admin/help.rb +396 -0
  69. data/lib/d3/admin/interactive.rb +972 -0
  70. data/lib/d3/admin/options.rb +454 -0
  71. data/lib/d3/admin/prefs.rb +204 -0
  72. data/lib/d3/admin/report.rb +727 -0
  73. data/lib/d3/admin/state.rb +42 -0
  74. data/lib/d3/admin/validate.rb +413 -0
  75. data/lib/d3/admin.rb +42 -0
  76. data/lib/d3/basename.rb +217 -0
  77. data/lib/d3/client/auth.rb +108 -0
  78. data/lib/d3/client/class_methods.rb +766 -0
  79. data/lib/d3/client/class_variables.rb +47 -0
  80. data/lib/d3/client/cli.rb +187 -0
  81. data/lib/d3/client/environment.rb +134 -0
  82. data/lib/d3/client/help.rb +110 -0
  83. data/lib/d3/client/lists.rb +314 -0
  84. data/lib/d3/client/receipt.rb +1173 -0
  85. data/lib/d3/client.rb +45 -0
  86. data/lib/d3/configuration.rb +319 -0
  87. data/lib/d3/constants.rb +60 -0
  88. data/lib/d3/database.rb +488 -0
  89. data/lib/d3/exceptions.rb +44 -0
  90. data/lib/d3/log.rb +271 -0
  91. data/lib/d3/package/aliases.rb +80 -0
  92. data/lib/d3/package/attributes.rb +97 -0
  93. data/lib/d3/package/class_methods.rb +817 -0
  94. data/lib/d3/package/class_variables.rb +46 -0
  95. data/lib/d3/package/client_actions.rb +293 -0
  96. data/lib/d3/package/constants.rb +58 -0
  97. data/lib/d3/package/constructor.rb +191 -0
  98. data/lib/d3/package/getters.rb +164 -0
  99. data/lib/d3/package/mixins.rb +39 -0
  100. data/lib/d3/package/private_methods.rb +227 -0
  101. data/lib/d3/package/questions.rb +95 -0
  102. data/lib/d3/package/server_actions.rb +683 -0
  103. data/lib/d3/package/setters.rb +326 -0
  104. data/lib/d3/package/validate.rb +448 -0
  105. data/lib/d3/package.rb +51 -0
  106. data/lib/d3/puppytime/pending_puppy.rb +108 -0
  107. data/lib/d3/puppytime/puppy_queue.rb +274 -0
  108. data/lib/d3/puppytime.rb +68 -0
  109. data/lib/d3/state.rb +105 -0
  110. data/lib/d3/utility.rb +325 -0
  111. data/lib/d3/version.rb +1 -1
  112. metadata +162 -9
@@ -0,0 +1,817 @@
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 D3
27
+ class Package < JSS::Package
28
+
29
+ ################# Class Methods #################
30
+
31
+ ###### These methods return Hashes or Arrays of
32
+ # data about d3 packages without instantiating
33
+ # the objects themselves
34
+
35
+ ### Raw(ish) SQL data for all d3 packages as a Hash of Hashes.
36
+ ###
37
+ ### The keys are the JSS ids of the packages
38
+ ###
39
+ ### The values are records from the d3_packages table, as Hashes
40
+ ### with keys matching the keys of {D3::Database::PACKAGE_TABLE}[:field_definitions]
41
+ ### plus these fields from the JSS's packages table:
42
+ ### :name, :require_reboot, :oses, :required_processor
43
+ ###
44
+ ### This raw data, queried directly via SQL, lets
45
+ ### us process lists of packages without instantiating
46
+ ### each one as a D3::Package object, which is slow
47
+ ### due to API calls for each package.
48
+ ### We can use this data to instantiate
49
+ ### those package objects when they are actually needed.
50
+ ###
51
+ ### @param refresh[Boolean] should the data be re-read from the database?
52
+ ###
53
+ ### @param status[Symbol] return only data for packages with this status. See {D3::Basename::STATUSES}, defaults to :all
54
+ ###
55
+ ### @return [Hash<Hash>] database data about all packages known to d3
56
+ ###
57
+ def self.package_data(refresh = false, status = :all)
58
+ @@package_data = nil if refresh
59
+
60
+ if @@package_data.nil?
61
+
62
+ # get a few fields from the JSS package data
63
+ # store them in a hash keyed by id
64
+ #jss_q = "SELECT package_id, package_name, os_requirements, require_reboot, required_processor FROM #{JSS::Package::DB_TABLE} WHERE package_id IN (SELECT package_id from #{P_TABLE[:table_name]})"
65
+ jss_q = "SELECT package_id, package_name, os_requirements, require_reboot, required_processor, allow_uninstall FROM #{JSS::Package::DB_TABLE}"
66
+
67
+ r = JSS::DB_CNX.db.query jss_q
68
+ jss_data = {}
69
+ r.each_hash{|jpkg| jss_data[jpkg["package_id"].to_i] = jpkg }
70
+
71
+ r.free
72
+
73
+ # get the table records and add in the appropriate names
74
+ @@package_data = {}
75
+
76
+ D3::Database.table_records(D3::Package::P_TABLE).each do |p|
77
+ d3_id = p[:id]
78
+
79
+ @@package_data[d3_id] = p
80
+
81
+ @@package_data[d3_id][:edition] = "#{p[:basename]}-#{p[:version]}-#{p[:revision]}"
82
+
83
+ # D3::Database.table_records returns NULL as nil even if converting with STRING_TO_INT
84
+ # which in general is a good thing, but expiration should always be an integer and NULL should be 0
85
+ @@package_data[d3_id][:expiration] = p[:expiration].to_i
86
+
87
+ jinfo = jss_data[d3_id]
88
+
89
+ # is this pkg still in the JSS?
90
+ if jinfo
91
+ @@package_data[d3_id][:name] = jinfo["package_name"]
92
+ @@package_data[d3_id][:oses] = jinfo["os_requirements"]
93
+ @@package_data[d3_id][:reboot] = (jinfo["require_reboot"] == "1") # boolean
94
+ @@package_data[d3_id][:required_processor] = jinfo["required_processor"]
95
+ @@package_data[d3_id][:removable] = (jinfo["allow_uninstall"] == "1")
96
+ # or is it missing?
97
+ else
98
+ @@package_data[d3_id][:status] = :missing
99
+ @@package_data[d3_id][:name] = "** missing from jss **"
100
+ @@package_data[d3_id][:oses] = ""
101
+ @@package_data[d3_id][:reboot] = false
102
+ @@package_data[d3_id][:required_processor] = "None"
103
+ @@package_data[d3_id][:removable] = false
104
+ end # if jinfo
105
+ end # D3::Database.table_records(D3::Package::P_TABLE).each do |p|
106
+ end # if @@package_data.nil?
107
+
108
+ if status == :all
109
+ return @@package_data
110
+ else
111
+ raise JSS::InvalidDataError, "status must be one of :#{STATUSES.join(', :')}" unless STATUSES.include? status
112
+ # reject because Hash#select returns an array of arrays
113
+ return @@package_data.reject{|id,p| p[:status] != status }
114
+ end #if status == :all
115
+ end # self.package_data(refresh = false, status = :all)
116
+
117
+ ### A Hash of package identifiers (id's, names, editions)
118
+ ### as keys, to the status of the package (symbols)
119
+ ###
120
+ ### @param identifier[Symbol] one of :id, :name, or :edition
121
+ ###
122
+ ### @return [Hash{String,Integer => Symbol] the statuses of the packages
123
+ ###
124
+ def self.statuses_by (identifier, refresh = false)
125
+ raise JSS::InvalidDataError, "identifier must be one of :id, :edition, or :name" unless [:id, :name, :edition].include? identifier
126
+ stati = {}
127
+ self.package_data(refresh).values.each{|pkg| stati[pkg[identifier]] = pkg[:status] }
128
+ stati
129
+ end # statuses by
130
+
131
+ ### A Hash mapping package ids to editions in d3
132
+ ###
133
+ ### A package's 'edition' is the combination of its basename, version, and revision,
134
+ ### joined with hyphens into a String, which must be unique in d3.
135
+ ###
136
+ ### @param refresh [Boolean] should the data be re-read from the database?
137
+ ###
138
+ ### @return [Hash{Integer => String}] the current editions in d3
139
+ ###
140
+ def self.ids_to_editions(refresh = false )
141
+ pd = self.package_data(refresh)
142
+ pd.merge(pd){|id,p| p[:edition]}
143
+ end
144
+
145
+ ### An Array of ids for all pkgs with a given basename
146
+ ###
147
+ ### @param basename[String] the basename to look for
148
+ ###
149
+ ### @param refresh [Boolean] should the data be re-read from the database?
150
+ ###
151
+ ### @return [Array] The package ids with the desired basename
152
+ ###
153
+ def self.ids_for_basename(basename, refresh = false)
154
+ self.package_data(refresh).values.select{|p| p[:basename] == basename}.map{|p| p[:id]}
155
+ end
156
+
157
+ ### An Array of all packages ids known to d3
158
+ ###
159
+ ### @param refresh [Boolean] should the data be re-read from the database?
160
+ ###
161
+ ### @return [Array<Integer>] the pkg ids known to d3
162
+ ###
163
+ def self.all_ids(refresh = false)
164
+ self.package_data(refresh).keys
165
+ end
166
+
167
+ ### An Array of all packages names known to d3
168
+ ###
169
+ ### @param refresh [Boolean] should the data be re-read from the database?
170
+ ###
171
+ ### @return [Array<String>] the pkg names known to d3
172
+ ###
173
+ def self.all_names(refresh = false)
174
+ self.package_data(refresh).values.map{|p| p[:name]}
175
+ end
176
+
177
+ ### An Array of basenames known to d3
178
+ ###
179
+ ### @param refresh [Boolean] should the data be re-read from the database?
180
+ ###
181
+ ### @return [Array<String>] the basenames known to d3
182
+ ###
183
+ def self.all_basenames(refresh = false)
184
+ self.package_data(refresh).values.map{|p| p[:basename] }.uniq
185
+ end
186
+
187
+ ### An Array of editions known to d3
188
+ ###
189
+ ### @param refresh [Boolean] should the data be re-read from the database?
190
+ ###
191
+ ### @return [Array<String>] the basenames known to d3
192
+ ###
193
+ def self.all_editions(refresh = false)
194
+ self.package_data(refresh).values.map{|p| p[:edition] }
195
+ end
196
+
197
+ ### A Hash of all packages filenames keyed by pkg id
198
+ ### These are looked up via a DB query because otherwise
199
+ ### we'd have to instantiate a Package object for every package
200
+ ### which is way too slow.
201
+ ###
202
+ ### @param refresh [Boolean] should the data be re-read from the database?
203
+ ###
204
+ ### @return [Hash{Integer: String}] the pkg filenames
205
+ ###
206
+ def self.all_filenames(refresh = false)
207
+ @@filenames = nil if refresh
208
+ return @@filenames if @@filenames
209
+ @@filenames = {}
210
+ qr = JSS::DB_CNX.db.query "SELECT package_id, file_name FROM packages"
211
+ qr.each_hash{|p| @@filenames[p["package_id"].to_i] = p["file_name"]}
212
+ qr.free
213
+ @@filenames
214
+ end
215
+
216
+ ### A Hash of Package Data for all live packages
217
+ ###
218
+ ### This is the {D3::Package.package_data} Hash, limited to
219
+ ### those packages whose status is :live, i.e. the package
220
+ ### that gets installed for its basename.
221
+ ###
222
+ ### @return [Hash{Integer=>Hash}] The live pacakge data
223
+ ###
224
+ def self.live_data(refresh = false)
225
+ self.package_data(refresh, :live)
226
+ end
227
+
228
+ ### A Hash mapping all basenames to their currently live jss id
229
+ ###
230
+ ### @param refresh [Boolean] should the data be re-read from the database?
231
+ ###
232
+ ### @return [Hash{String => Integer}] The basenames and id's of all live packages
233
+ ###
234
+ def self.basenames_to_live_ids(refresh = false)
235
+ # Hashes don't have #map, so #merge back onto ourselves to have the
236
+ # same effect.
237
+ lp = self.live_data(refresh)
238
+ lp.merge(lp){|id,p| p[:basename] }.invert
239
+ end
240
+
241
+ ### An Array of all packages ids that are live
242
+ ###
243
+ ### @param refresh [Boolean] should the data be re-read from the database?
244
+ ###
245
+ ### @return [Array<String>] the live pkg ids
246
+ ###
247
+ def self.live_ids(refresh = false)
248
+ self.live_data(refresh).keys
249
+ end
250
+
251
+ ### An Array of all packages names that are live
252
+ ###
253
+ ### @param refresh [Boolean] should the data be re-read from the database?
254
+ ###
255
+ ### @return [Array<String>] the pkg names known to d3
256
+ ###
257
+ def self.live_names(refresh = false)
258
+ self.live_data(refresh).values.map{|p| p[:name]}
259
+ end
260
+
261
+ ### An Array of basenames that have live packages.
262
+ ###
263
+ ### @param refresh [Boolean] should the data be re-read from the database?
264
+ ###
265
+ ### @return [Array] the live basenames
266
+ ###
267
+ def self.live_basenames(refresh = false)
268
+ self.basenames_to_live_ids(refresh).keys
269
+ end
270
+
271
+ ### A Hash of Package Data for all "pilot" packages
272
+ ###
273
+ ### This is the {D3::Package.package_data} Hash, limited to
274
+ ### those packages whose status is :pilot, i.e. they are not live
275
+ ### and are newer than the live pkg for their basename.
276
+ ###
277
+ ### @param refresh [Boolean] should the data be re-read from the database?
278
+ ###
279
+ ### @return [Hash<Hash>] the pilotable packages known to d3
280
+ ###
281
+ def self.pilot_data(refresh = false)
282
+ self.package_data(refresh, :pilot)
283
+ end
284
+
285
+ ### A Hash of Package Data for all "deprecated" packages
286
+ ###
287
+ ### This is the {D3::Package.package_data} Hash, limited to
288
+ ### those packages whose status is :depreicated. These packages
289
+ ### were once live, and still exist in the JSS and can
290
+ ### be made live again, though that isn't recommended.
291
+ ###
292
+ ### @param refresh [Boolean] should the data be re-read from the database?
293
+ ###
294
+ ### @return [Array<Hash>] the deprecated packages known to d3
295
+ ###
296
+ def self.deprecated_data(refresh = false)
297
+ self.package_data(refresh, :deprecated)
298
+ end
299
+
300
+ ### A Hash of Package Data for all "skipped" packages
301
+ ###
302
+ ### This is the {D3::Package.package_data} Hash, limited to
303
+ ### those packages whose status us :skipped - i.e. the were never
304
+ ### made live before a newer pkg of the same basename was made live.
305
+ ###
306
+ ### @param refresh [Boolean] should the data be re-read from the database?
307
+ ###
308
+ ### @return [Array<Hash>] the deprecated packages known to d3
309
+ ###
310
+ def self.skipped_data(refresh = false)
311
+ self.package_data(refresh, :skipped)
312
+ end
313
+
314
+ ### A Hash of Package Data for all "missing" packages
315
+ ###
316
+ ### This is the {D3::Package.package_data} Hash, limited to
317
+ ### those packages whose status us :missing - i.e. the package
318
+ ### id no longer exists in the JSS.
319
+ ###
320
+ ### @param refresh [Boolean] should the data be re-read from the database?
321
+ ###
322
+ ### @return [Array<Hash>] the missing packages known to d3
323
+ ###
324
+ def self.missing_data(refresh = false)
325
+ self.package_data(refresh, :missing)
326
+ end
327
+
328
+
329
+
330
+ ### Get a single D3::Package by using a search term.
331
+ ###
332
+ ### The term is searched in this order:
333
+ ### edition, basename, id, display name, filename.
334
+ ###
335
+ ### If basename, returns the currently live pkg
336
+ ###
337
+ ### The first match is returned, nil if no match
338
+ ###
339
+ ### @param search_term[String] the thing to look for
340
+ ###
341
+ ### @param type[Symbol] either :pkg to return a D3::Package or
342
+ ### :hash to return the raw D3::Package.package_data hash
343
+ ### for the matching package. This is quicker and doesn't
344
+ ### instantiate a Package Object from the API.
345
+ ###
346
+ ### @return [D3::Package, nil] the package that was found.
347
+ ###
348
+ def self.find_package(search_term, type = :pkg )
349
+ return nil if search_term.nil?
350
+
351
+ if self.all_editions.include? search_term
352
+ id = D3::Package.ids_to_editions.invert[search_term]
353
+
354
+ elsif self.all_basenames.include? search_term
355
+ id = self.basenames_to_live_ids[search_term]
356
+
357
+ elsif self.all_ids.include? search_term.to_i
358
+ id = search_term.to_i
359
+
360
+ elsif self.all_names.include? search_term
361
+ id = D3::Package.map_all_ids_to(:name).invert[search_term]
362
+
363
+ elsif self.all_filenames.values.include? search_term
364
+ id = self.all_filenames.invert[search_term]
365
+
366
+ else
367
+ return nil
368
+ end # if elsif.....
369
+
370
+ return nil unless id and id.is_a? Fixnum
371
+
372
+ return type == :pkg ? D3::Package.new(:id => id) : self.package_data[id]
373
+ end
374
+
375
+ ### Get the most recent package on the server
376
+ ### for a given basename
377
+ ###
378
+ ### @param basename[String] the basename to look for
379
+ ###
380
+ ### @return [D3::Package, nil] the most recent package, or nil if none
381
+ ###
382
+ def self.most_recent_package_for(basename)
383
+ return nil unless D3::Package.all_basenames.include? basename
384
+ # deal with potentially missing pkgs:
385
+ # start with the highest id until we find an existing one
386
+ self.ids_for_basename(basename).sort.reverse.each do |id|
387
+ begin
388
+ pkg = D3::Package.new :id => id
389
+ return pkg
390
+ rescue JSS::NoSuchItemError
391
+ next
392
+ end
393
+ end
394
+ return nil
395
+ end
396
+
397
+ ### A Hash of Hashes of all scripts used by d3 packages.
398
+ ### Each key is a script ID,
399
+ ### Each value is a sub-Hash with one entry per d3 pkg that uses the script.
400
+ ###
401
+ ### The sub-Hashes have pkg id's as keys, and an Array of
402
+ ### script usages as values. (since a pkg can use the same
403
+ ### script for any or all of the 4 script types)
404
+ ###
405
+ ### Example:
406
+ ### {
407
+ ### 123 => { 234 => [:pre_install] },
408
+ ###
409
+ ### 456 => { 234 => [:post_install],
410
+ ### 345 => [:post_install, :pre_remove] }
411
+ ### }
412
+ ###
413
+ ### In the example above,
414
+ ### - script id 123 is used by pkg id 234 as a pre-install script
415
+ ### - script id 456 is used by pkg id 234 as a post-install script
416
+ ### and used by pkg id 345 as both a post-install and pre-remove script.
417
+ ###
418
+ ### @param refresh [Boolean] should the data be re-read from the database?
419
+ ###
420
+ ### @return [Hash{Integer => Hash{Integer => Array<Symbol>}}] the scripts used by packages in d3
421
+ ###
422
+ def self.scripts(refresh = false)
423
+ scripts = {}
424
+ self.package_data(refresh, :all).each do |pkg_id, pkg_data|
425
+ SCRIPT_TYPES.each do |script_type|
426
+ script_id_key = "#{script_type}_script_id".to_sym
427
+ if pkg_data[script_id_key]
428
+ scr_id = pkg_data[script_id_key]
429
+ scripts[scr_id] ||= {}
430
+ scripts[scr_id][pkg_id] ||= []
431
+ scripts[scr_id][pkg_id] << script_type
432
+ end
433
+ end # each script type
434
+ end # do each pkg_id, pkg_data
435
+ scripts
436
+ end
437
+
438
+ ### An Array of pkg ids for all pkgs that use a given script,
439
+ ### optionally limiting to those pkgs that use the script
440
+ ### for a given purpose.
441
+ ###
442
+ ### @param script[Integer,String] The name or ID number of the script
443
+ ###
444
+ ### @param script_type[Symbol,nil] The script-type by which to limit the results.
445
+ ### One of SCRIPT_TYPES, or nil for all types
446
+ ###
447
+ ### @param refresh[Boolean] Should the data be re-read from the server?
448
+ ###
449
+ ### @return [Array<Integer>] the ids for each package that uses the script
450
+ ###
451
+ def self.packages_for_script (script, script_type = nil, refresh = false)
452
+
453
+ if script_type
454
+ raise JSS::InvalidDataError, "Script type must be one of :#{SCRIPT_TYPES.join(' :')}" unless SCRIPT_TYPES.include? script_type
455
+ end # if screipt type
456
+
457
+ pkgs = []
458
+
459
+ # confirm the ID of the script..
460
+ sid = JSS::Script.all_ids(refresh).include?(script) ? script : nil
461
+ sid ||= JSS::Script.map_all_ids_to(:name).invert[script]
462
+
463
+ # script id has to exist in JSS and some d3 pkg
464
+ return pkgs unless sid and self.scripts(refresh)[sid]
465
+
466
+ self.scripts(refresh)[sid].each do |pkg_id, uses|
467
+ if script_type
468
+ pkgs << pkg_id if uses.include? script_type
469
+ else
470
+ pkgs << pkg_id
471
+ end
472
+ end # each script id, pkgs
473
+
474
+ pkgs
475
+ end
476
+
477
+ ### An array of ids for all pkgs that are auto-installed for
478
+ ### a given computer group. Returns an empty array if no such group.
479
+ ###
480
+ ### @param group[String] the name of the JSS group for which to find
481
+ ### ids
482
+ ###
483
+ ### @param refresh[Boolean] should the data be re-read from the db?
484
+ ###
485
+ ### @return [Array<Integer>] the ids auto-installed for that group
486
+ ###
487
+ def self.auto_install_ids_for_group (group, refresh = false)
488
+ pkgs = D3::Package.package_data(refresh).values
489
+ pkgs.select{|p| p[:auto_groups].include? group}.map{|p| p[:id]}
490
+ end
491
+
492
+ ### An array of ids for all pkgs that are exclude for a given
493
+ ### computer group. Returns an empty array if no such group.
494
+ ###
495
+ ### @param group[String] the name of the JSS group for which to find
496
+ ### ids
497
+ ###
498
+ ### @param refresh[Boolean] should the data be re-read from the db?
499
+ ###
500
+ ### @return [Array<Integer>] the ids excluded for that group
501
+ ###
502
+ def self.exclude_ids_for_group (group, refresh = false)
503
+ pkgs = D3::Package.package_data(refresh).values
504
+ pkgs.select{|p| p[:excluded_groups].include? group}.map{|p| p[:id]}
505
+ end
506
+
507
+
508
+
509
+
510
+ ### Import an existing JSS::Package into d3.
511
+ ###
512
+ ### A d3 basename and version must be provided.
513
+ ###
514
+ ### If no revision is provided, it is set to 1.
515
+ ###
516
+ ### If the JSS package is an Apple installer pkg, the read-only password for the
517
+ ### current distribution point must be provided so that the Apple package
518
+ ### identifier(s) can be queried from the pkg on the server.
519
+ ###
520
+ ### After the D3::Package is instantiated, these and other d3-specific values
521
+ ### can be changed before creating it on the server.
522
+ ###
523
+ ### IMPORTANT: Even though the JSS package already exists, you must call
524
+ ### {#create} after instantiating this new D3::Package in order to save it
525
+ ### into d3.
526
+ ###
527
+ ### @param ident[String,Integer] the name or id of the JSS::Package to import.
528
+ ###
529
+ ### @param args[Hash] The d3-specific values for this package. Here are some
530
+ ### of the important ones for importing.
531
+ ### See {#initialize} for more details.
532
+ ###
533
+ ### @option args :basename[String] The d3 basename to which this package will belong
534
+ ###
535
+ ### @option args :version[String] The version of the thing installed.
536
+ ###
537
+ ### @option args :revision[Integer] The revision of this pkg version in d3. Defaults to 1.
538
+ ###
539
+ ### @option args :dist_pw[String] The read-only or read-write password for the distribution point for this machine.
540
+ ###
541
+ ### @option args :unmount[Boolean] Should the dist.point be unmounted after this? Defaults to true
542
+ ###
543
+ ### @return [D3::Package] The newly imported d3 package object,
544
+ ### not yet saved as a d3 pkg on the server
545
+ ###
546
+ def self.import (ident, args)
547
+ id = if JSS::Package.all_ids.include? ident
548
+ ident
549
+ else
550
+ JSS::Package.map_all_ids_to(:name).invert[ident]
551
+ end
552
+ raise JSS::NoSuchItemError, "No JSS Package with name or id matching '#{ident}'" unless id
553
+ raise JSS::AlreadyExistsError, "That JSS package already exists in d3" if self.all_ids.include? id
554
+
555
+ raise JSS::MissingDataError, "Importing packages requires :basename" unless args[:basename]
556
+ raise JSS::MissingDataError, "Importing packages requires :version" if args[:version].to_s.strip.empty?
557
+
558
+ args[:revision] ||= 1
559
+
560
+ jss_pkg = JSS::Package.new :id => id
561
+
562
+ tmp_edition = "#{args[:basename]}-#{args[:version]}-#{args[:revision]}"
563
+ if self.all_editions.include? tmp_edition
564
+ raise JSS::InvalidDataError, "A d3 pkg for edition #{tmp_edition} already exists."
565
+ end # unless
566
+
567
+ args[:id] = id
568
+ args[:import] = true
569
+ args[:unmount] = true if args[:unmount].nil?
570
+
571
+ imported_pkg = self.new(args)
572
+ imported_pkg.update_apple_receipt_data args[:dist_pw], args[:unmount]
573
+ imported_pkg
574
+ end
575
+
576
+ ### Check for existence of one or more computer groups in the JSS,
577
+ ### raise an exception if any group doesn't exist.
578
+ ###
579
+ ### @param groups[String,Array<String>] the group name(s) to check, if string, comma-separated.
580
+ ###
581
+ ### @return [Array<String>] valid, existing group names.
582
+ ###
583
+ def self.check_computer_groups(groups)
584
+ parsed_groups = JSS.to_s_and_a(groups)
585
+ parsed_groups[:arrayform].each do |g|
586
+ raise JSS::NoSuchItemError, "No ComputerGroup named '#{g}' in the JSS" unless JSS::ComputerGroup.all_names.include? g
587
+ end
588
+ return parsed_groups[:arrayform]
589
+ end
590
+
591
+ ### Givin a Pathname to a package, return an array of
592
+ ### hashes with data for all the pkg rcpts that will be installed
593
+ ### each hash includes at least :apple_pkg_id, :version, and :installed_kb
594
+ ###
595
+ ### Thanks to Greg Neagle for inspiration on this method from munkicommon.py
596
+ ###
597
+ ### @param pkg_path[String,Pathname] the path to a .pkg to scan
598
+ ###
599
+ ### @return [Array<Hash>] the Apple receipt data for the pkg
600
+ ###
601
+ def self.receipt_data_from_pkg (pkg_path)
602
+ pkg_path = Pathname.new(pkg_path) unless pkg_path.is_a? Pathname
603
+ raise "The path given must end with .pkg or .mpkg" unless pkg_path.to_s =~ PKG_RE
604
+ raise "Package '#{pkg_path}' doesn't exist" unless pkg_path.exist?
605
+
606
+ if pkg_path.directory?
607
+ self.receipt_data_from_bundle_pkg(pkg_path).uniq
608
+ else
609
+ self.receipt_data_from_flat_pkg(pkg_path).uniq
610
+ end
611
+ end # def receipt_data_from_pkg (pkg_path)
612
+
613
+ ### Givinn a Pathname to a flat package, return an array of
614
+ ### hashes with data for all the pkg rcpts hat will be installed
615
+ ### each has includes at least :apple_pkg_id, :version, and :installed_kb
616
+ ###
617
+ ### Thanks to Greg Neagle for inspiration on this method from munkicommon.py
618
+ ###
619
+ def self.receipt_data_from_flat_pkg (pkg_path)
620
+
621
+ pkg_path = Pathname.new(pkg_path)
622
+ rcpts = []
623
+
624
+ pkg_contents = `/usr/bin/xar -tf #{Shellwords.escape pkg_path.to_s}`
625
+ raise "Could not look at contents of flat package #{pkg_path.basename}" unless $CHILD_STATUS.exitstatus == 0
626
+
627
+ start_dir = Pathname.pwd
628
+ work_dir = Pathname.new Dir.mktmpdir
629
+ Dir.chdir work_dir
630
+
631
+ begin
632
+ # loop thru the items in the flat pkg
633
+ # extracting any PackageInfo and Distribution files
634
+ xml_files = []
635
+
636
+ pkg_contents.each_line do |line|
637
+ line.chomp!
638
+
639
+ # if there's a top level Dist or PackageInfo, use it exclusively
640
+ if line. == "PackageInfo" or line == "Distribution"
641
+ xml_files = [line]
642
+ break
643
+
644
+ # otherwise find all sub PackageInfos
645
+ elsif line.end_with? ".pkg/PackageInfo"
646
+ xml_files << line
647
+ end
648
+
649
+ end # pkg_contents.each_line do line
650
+
651
+ # Extract whatever files we found interesting
652
+ xml_files.each do |xml_file|
653
+ system "/usr/bin/xar", "-xf", pkg_path.to_s, xml_file
654
+ raise raise "Error reading #{xml_file} from flat package #{pkg_path.basename}" unless $CHILD_STATUS.exitstatus == 0
655
+ extracted_file = work_dir + xml_file
656
+ rcpts += self.receipt_data_from_xml(extracted_file, pkg_path)
657
+ end # xml files.each
658
+
659
+ ensure
660
+ Dir.chdir start_dir
661
+ work_dir.rmtree
662
+ end # begin
663
+
664
+ rcpts
665
+
666
+ end # def receipt_data_from_flat_pkg (pkg_path)
667
+
668
+ ### givin a Pathname to a bundle pkg, return an array of
669
+ ### hashes with data for all the pkg rcpts hat will be installed
670
+ ### each has includes at least :apple_pkg_id, :version, and :installed_kb
671
+ ###
672
+ ### Thanks to Greg Neagle for inspiration on this method from munkicommon.py
673
+ ###
674
+ def self.receipt_data_from_bundle_pkg (pkg_path)
675
+ pkg_path = Pathname.new(pkg_path) unless pkg_path.is_a? Pathname
676
+ rcpts = []
677
+
678
+ # if this is a single pkg, not a metapkg, data comes from Info.plist
679
+ if pkg_path.to_s.end_with? ".pkg"
680
+ rcpt_data = {}
681
+ info_plist = pkg_path + "Contents/Info.plist"
682
+ if info_plist.exist?
683
+ plist = D3.parse_plist info_plist
684
+ rcpt_data[:apple_pkg_id] = plist["CFBundleIdentifier"]
685
+ rcpt_data[:apple_pkg_id] ||= plist["Bundle Identifier"]
686
+ rcpt_data[:apple_pkg_id] ||= pkg_path.basename.to_s
687
+
688
+ rcpt_data[:installed_kb] = plist["IFPkgFlagInstalledSize"]
689
+
690
+ rcpt_data[:version] = plist["CFBundleShortVersionString"]
691
+ rcpt_data[:version] ||= plist["CFBundleVersion"]
692
+ rcpt_data[:version] ||= plist["Bundle versions string, short"]
693
+
694
+ rcpts << rcpt_data
695
+ end # if plist exist?
696
+
697
+ end
698
+
699
+ # if rcpts is empty, it could be an mpkg, which installs more than one pkg
700
+ if rcpts.empty?
701
+ contents_dir = info_plist = pkg_path + "Contents"
702
+ dist_file = contents_dir.children.select{|c| c.to_s.end_with? ".dist"}[0]
703
+ return self.receipt_data_from_xml(dist_file, pkg_path) if dist_file
704
+
705
+ # no dist file - any other embedded packages?
706
+ pkg_path.find do |sub_item|
707
+ next unless sub_item.to_s =~ PKG_RE
708
+ rcpts += self.receipt_data_from_bundle_pkg(sub_item)
709
+ end # pkg_path.find
710
+ end # if pkg_path.to_s.end_with? ".pkg"
711
+
712
+ rcpts
713
+
714
+ end # def receipt_data_from_bundle_pkg (pkg_path)
715
+
716
+ ### Parse an xml file (like a .dist, Distribution, or PackageInfo file) and find all pkg-ref or pkg-info elements
717
+ ### to extract their pkg ids and other data, or locate and recurse on any sub-pkgs.
718
+ ### Return an array of hashes of pkg data.
719
+ ### xml_file_path is a String or Pathname to the xml file. If the xml file is not
720
+ ### embedded in a pkg (eg it was extracted from a flat pkg), provide the path to the pkg as the second arg.
721
+ ###
722
+ ### Thanks to Greg Neagle for inspiration on this method from munkicommon.py
723
+ ###
724
+ def self.receipt_data_from_xml(xml_file_path, pkg_path = nil)
725
+
726
+ # both args must be Pathnames if they're strings
727
+ xml_file_path = Pathname.new(xml_file_path) unless xml_file_path.is_a? Pathname
728
+ pkg_path = Pathname.new(pkg_path) if pkg_path and !pkg_path.is_a?(Pathname)
729
+
730
+ # this will be returned - an array of hashes of data about this pkg and sub-pkgs
731
+ rcpts = []
732
+
733
+ # parse the xml
734
+ doc = REXML::Document.new(File.new(xml_file_path))
735
+
736
+ ####
737
+ # pkg-info elements
738
+ doc.elements.each("//pkg-info") do |pkg_info_element|
739
+
740
+ attribs = pkg_info_element.attributes
741
+
742
+ # we only care about elements with both an identifier and a version
743
+ next unless attribs["identifier"] && attribs["version"]
744
+
745
+ data = { :apple_pkg_id => attribs["identifier"], :version => attribs["version"] }
746
+
747
+ payload = pkg_info_element.elements.to_a('payload')[0]
748
+ data[:installed_kb] = payload.attributes["installKBytes"].to_i if payload.attributes["installKBytes"]
749
+
750
+ rcpts << data unless rcpts.include? data
751
+ return rcpts unless rcpts.empty?
752
+ end # doc.elements.each("*/pkg-ref") do |pkg|
753
+
754
+ ####
755
+ # pkg-ref elements
756
+ all_ref_data = {}
757
+ doc.elements.each("//pkg-ref") do |pkg_ref_element|
758
+
759
+ attribs = pkg_ref_element.attributes
760
+
761
+ next unless attribs["id"] && attribs["version"]
762
+
763
+ # make a new hash for this pkg if needed
764
+ this_ref_data = {:apple_pkg_id => attribs["id"]}
765
+
766
+ # any inner-content of the element is a path to a sub-pkg
767
+ if pkg_ref_element.text
768
+ this_ref_data[:sub_pkg_ref] = pkg_ref_element.text
769
+
770
+ # if its a file: url, its relative to the pkg_path
771
+ if this_ref_data[:sub_pkg_ref] =~ /^file:.*\.pkg$/
772
+ this_ref_data[:sub_pkg_path] = (pkg_path || xml_file_path.dirname) + URI.decode(this_ref_data[:sub_pkg_ref][5..-1])
773
+
774
+ # but it might be a relative path from the cwd, starting with a #, which suould be ignored.
775
+ elsif this_ref_data[:sub_pkg_ref] =~ /^#.*\.pkg$/
776
+ this_ref_data[:sub_pkg_path] = xml_file_path.dirname + URI.decode(this_ref_data[:sub_pkg_ref][1..-1])
777
+ end # if this_ref_data[:sub_pkg_ref] =~ /^file:.*\.pkg$/
778
+
779
+ end # pkg_ref_element.text
780
+
781
+ this_ref_data[:version] = attribs["version"]
782
+ this_ref_data[:installed_kb] = attribs["installKBytes"].to_i
783
+ this_ref_data[:auth] = attribs["auth"]
784
+
785
+ sub_pkg_data = []
786
+
787
+ # if we have a file path to an existing pkg, try to get data from it rather than from this xml
788
+ if this_ref_data[:sub_pkg_path] and this_ref_data[:sub_pkg_path].exist?
789
+ sub_pkg_data = receipt_data_from_pkg(this_ref_data[:sub_pkg_path])
790
+ end
791
+
792
+ # did the sub pkg have data?
793
+ # if not, use the data from this xml, as long as we have a version
794
+ #
795
+ if sub_pkg_data.empty?
796
+ if this_ref_data[:version]
797
+ this_ref_data.delete :sub_pkg_path
798
+ rcpts << this_ref_data
799
+ end
800
+
801
+ else
802
+ # but if it did, then use the data from the sub pkg.
803
+ rcpts += sub_pkg_data
804
+
805
+ end # this_ref_data[:sub_pkg_path]
806
+
807
+ end # doc.elements.each("*/pkg-ref") do |pkg_ref_element|
808
+
809
+ # clean up each rcpt hash, subpkg ref isn't needed any more
810
+ rcpts.each{|r| r.delete :sub_pkg_ref }
811
+
812
+ rcpts
813
+
814
+ end
815
+
816
+ end # class Package
817
+ end # module D3