• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

yast / yast-autoinstallation / 7347071288

28 Dec 2023 11:13AM UTC coverage: 69.009% (+0.4%) from 68.607%
7347071288

push

github

web-flow
Merge pull request #874 from yast/rubocop_update

Rubocop update

94 of 254 new or added lines in 51 files covered. (37.01%)

22 existing lines in 13 files now uncovered.

6373 of 9235 relevant lines covered (69.01%)

10.29 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

73.97
/src/modules/Profile.rb
1
# File:  modules/Profile.ycp
2
# Module:  Auto-Installation
3
# Summary:  Profile handling
4
# Authors:  Anas Nashif <nashif@suse.de>
5
#
6
# $Id$
7
require "yast"
1✔
8
require "yast2/popup"
1✔
9

10
require "fileutils"
1✔
11

12
require "autoinstall/entries/registry"
1✔
13
require "installation/autoinst_profile/element_path"
1✔
14
require "ui/password_dialog"
1✔
15

16
module Yast
1✔
17
  # Wrapper class around Hash to hold the autoyast profile.
18
  #
19
  # Rationale:
20
  #
21
  # The profile parser returns an empty String for empty elements like
22
  # <foo/> - and not nil. This breaks the code assumption that you can write
23
  # xxx.fetch("foo", {}) in a lot of code locations.
24
  #
25
  # To make access to profile elements easier this class provides methods
26
  # #fetch_as_hash and #fetch_as_array that check the expected type and
27
  # return the default value also if there is a type mismatch.
28
  #
29
  # See bsc#1180968 for more details.
30
  #
31
  # The class constructor converts an existing Hash to a ProfileHash.
32
  #
33
  class ProfileHash < Hash
1✔
34
    include Yast::Logger
1✔
35

36
    # Replace Hash -> ProfileHash recursively.
37
    def initialize(default = {})
1✔
38
      super()
2,802✔
39

40
      default.each_pair do |key, value|
2,802✔
41
        self[key] = value.is_a?(Hash) ? ProfileHash.new(value) : value
1,770✔
42
      end
43
    end
44

45
    # Read element from ProfileHash.
46
    #
47
    # @param key [String] the key
48
    # @param default [Hash] default value - returned if element does not exist or has wrong type
49
    #
50
    # @return [ProfileHash]
51
    def fetch_as_hash(key, default = {})
1✔
52
      fetch_as(key, Hash, default)
1,937✔
53
    end
54

55
    # Read element from ProfileHash.
56
    #
57
    # @param key [String] the key
58
    # @param default [Array] default value - returned if element does not exist or has wrong type
59
    #
60
    # @return [Array]
61
    def fetch_as_array(key, default = [])
1✔
62
      fetch_as(key, Array, default)
1,150✔
63
    end
64

65
  private
1✔
66

67
    # With an explicit default it's possible to check for the presence of an
68
    # element vs. empty element, if needed.
69
    def fetch_as(key, type, default = nil)
1✔
70
      tmp = fetch(key, nil)
3,087✔
71
      if !tmp.is_a?(type)
3,087✔
72
        f = caller_locations(2, 1).first
2,465✔
73
        if !tmp.nil?
2,465✔
74
          log.warn "AutoYaST profile type mismatch (from #{f}): " \
×
75
                   "#{key}: expected #{type}, got #{tmp.class}"
76
        end
77
        tmp = default.is_a?(Hash) ? ProfileHash.new(default) : default
2,465✔
78
      end
79
      tmp
3,087✔
80
    end
81
  end
82

83
  class ProfileClass < Module
1✔
84
    include Yast::Logger
1✔
85

86
    # Sections that are handled by AutoYaST clients included in autoyast2 package.
87
    AUTOYAST_CLIENTS = [
88
      "files",
1✔
89
      "general",
90
      # FIXME: Partitioning should probably not be here. There is no
91
      # partitioning_auto client. Moreover, it looks pointless to enforce the
92
      # installation of autoyast2 only because the <partitioning> section
93
      # is in the profile. It will happen on 1st stage anyways.
94
      "partitioning",
95
      "report",
96
      "scripts",
97
      "software"
98
    ].freeze
99

100
    def main
1✔
101
      Yast.import "UI"
8✔
102
      textdomain "autoinst"
8✔
103

104
      Yast.import "AutoinstConfig"
8✔
105
      Yast.import "AutoinstFunctions"
8✔
106
      Yast.import "Directory"
8✔
107
      Yast.import "GPG"
8✔
108
      Yast.import "Label"
8✔
109
      Yast.import "Mode"
8✔
110
      Yast.import "Popup"
8✔
111
      Yast.import "ProductControl"
8✔
112
      Yast.import "Stage"
8✔
113
      Yast.import "XML"
8✔
114

115
      Yast.include self, "autoinstall/xml.rb"
8✔
116

117
      # The Complete current Profile
118
      @current = Yast::ProfileHash.new
8✔
119

120
      @changed = false
8✔
121

122
      @prepare = true
8✔
123
      Profile()
8✔
124
    end
125

126
    # Constructor
127
    # @return [void]
128
    def Profile
1✔
129
      #
130
      # setup profile XML parameters for writing
131
      #
132
      profileSetup
8✔
133
      SCR.Execute(path(".target.mkdir"), AutoinstConfig.profile_dir) if Stage.initial
8✔
134
      nil
135
    end
136

137
    def softwareCompat
1✔
138
      @current["software"] = @current.fetch_as_hash("software")
106✔
139

140
      # We need to check if second stage was disabled in the profile itself
141
      # because AutoinstConfig is not initialized at this point
142
      # and InstFuntions#second_stage_required? depends on that module
143
      # to check if 2nd stage is required (chicken-and-egg problem).
144
      mode = @current.fetch_as_hash("general").fetch_as_hash("mode")
106✔
145
      second_stage_enabled = mode.key?("second_stage") ? mode["second_stage"] : true
106✔
146

147
      add_autoyast_packages if AutoinstFunctions.second_stage_required? && second_stage_enabled
106✔
148

149
      # workaround for missing "REQUIRES" in content file to stay backward compatible
150
      # FIXME: needs a more sophisticated or compatibility breaking solution after SLES11
151
      patterns = @current["software"].fetch_as_array("patterns")
106✔
152
      patterns = ["base"] if patterns.empty?
106✔
153
      @current["software"]["patterns"] = patterns
106✔
154

155
      nil
156
    end
157

158
    # compatibility to new language,keyboard and timezone client in 10.1
159
    def generalCompat
1✔
160
      if Builtins.haskey(@current, "general")
73✔
161
        if Builtins.haskey(Ops.get_map(@current, "general", {}), "keyboard")
9✔
162
          Ops.set(
×
163
            @current,
164
            "keyboard",
165
            Ops.get_map(@current, ["general", "keyboard"], {})
166
          )
167
          Ops.set(
×
168
            @current,
169
            "general",
170
            Builtins.remove(Ops.get_map(@current, "general", {}), "keyboard")
171
          )
172
        end
173
        if Builtins.haskey(Ops.get_map(@current, "general", {}), "language")
9✔
174
          Ops.set(
×
175
            @current,
176
            "language",
177
            "language" => Ops.get_string(
178
              @current,
179
              ["general", "language"],
180
              ""
181
            )
182
          )
183
          Ops.set(
×
184
            @current,
185
            "general",
186
            Builtins.remove(Ops.get_map(@current, "general", {}), "language")
187
          )
188
        end
189
        if Builtins.haskey(Ops.get_map(@current, "general", {}), "clock")
9✔
190
          Ops.set(
×
191
            @current,
192
            "timezone",
193
            Ops.get_map(@current, ["general", "clock"], {})
194
          )
195
          Ops.set(
×
196
            @current,
197
            "general",
198
            Builtins.remove(Ops.get_map(@current, "general", {}), "clock")
199
          )
200
        end
201
        if Ops.get_boolean(@current, ["general", "mode", "final_halt"], false) &&
9✔
202
            !Ops.get_list(@current, ["scripts", "init-scripts"], []).include?(HALT_SCRIPT)
203

204
          Ops.set(@current, "scripts", {}) if !Builtins.haskey(@current, "scripts")
×
205
          if !Builtins.haskey(
×
206
            Ops.get_map(@current, "scripts", {}),
207
            "init-scripts"
208
          )
209
            Ops.set(@current, ["scripts", "init-scripts"], [])
×
210
          end
211
          Ops.set(
×
212
            @current,
213
            ["scripts", "init-scripts"],
214
            Builtins.add(
215
              Ops.get_list(@current, ["scripts", "init-scripts"], []),
216
              HALT_SCRIPT
217
            )
218
          )
219
        end
220
        if Ops.get_boolean(@current, ["general", "mode", "final_reboot"], false) &&
9✔
221
            !Ops.get_list(@current, ["scripts", "init-scripts"], []).include?(REBOOT_SCRIPT)
222

223
          Ops.set(@current, "scripts", {}) if !Builtins.haskey(@current, "scripts")
1✔
224
          if !Builtins.haskey(
1✔
225
            Ops.get_map(@current, "scripts", {}),
226
            "init-scripts"
227
          )
228
            Ops.set(@current, ["scripts", "init-scripts"], [])
1✔
229
          end
230
          Ops.set(
1✔
231
            @current,
232
            ["scripts", "init-scripts"],
233
            Builtins.add(
234
              Ops.get_list(@current, ["scripts", "init-scripts"], []),
235
              REBOOT_SCRIPT
236
            )
237
          )
238
        end
239
        if Builtins.haskey(
9✔
240
          Ops.get_map(@current, "software", {}),
241
          "additional_locales"
242
        )
243
          Ops.set(@current, "language", {}) if !Builtins.haskey(@current, "language")
×
244
          Ops.set(
×
245
            @current,
246
            ["language", "languages"],
247
            Builtins.mergestring(
248
              Ops.get_list(@current, ["software", "additional_locales"], []),
249
              ","
250
            )
251
          )
252
          Ops.set(
×
253
            @current,
254
            "software",
255
            Builtins.remove(
256
              Ops.get_map(@current, "software", {}),
257
              "additional_locales"
258
            )
259
          )
260
        end
261
      end
262

263
      nil
264
    end
265

266
    # Read Profile properties and Version
267
    # @param properties [ProfileHash] Profile Properties
268
    # @return [void]
269
    def check_version(properties)
1✔
270
      version = properties["version"]
72✔
271
      if version == "3.0"
72✔
UNCOV
272
        log.info("AutoYaST Profile Version #{version} detected.")
×
273
      else
274
        log.info("Wrong profile version #{version}")
72✔
275
      end
276
    end
277

278
    # Import Profile
279
    # @param [Hash{String => Object}] profile
280
    # @return [void]
281
    def Import(profile)
1✔
282
      log.info("importing profile")
72✔
283

284
      profile = deep_copy(profile)
72✔
285
      @current = Yast::ProfileHash.new(profile)
72✔
286

287
      check_version(@current.fetch_as_hash("properties"))
72✔
288

289
      # old style
290
      if Builtins.haskey(profile, "configure") ||
72✔
291
          Builtins.haskey(profile, "install")
292
        configure = Ops.get_map(profile, "configure", {})
4✔
293
        install = Ops.get_map(profile, "install", {})
4✔
294
        @current = Builtins.remove(@current, "configure") if Builtins.haskey(profile, "configure")
4✔
295
        @current = Builtins.remove(@current, "install") if Builtins.haskey(profile, "install")
4✔
296
        tmp = Convert.convert(
4✔
297
          Builtins.union(configure, install),
298
          from: "map",
299
          to:   "map <string, any>"
300
        )
301
        @current = Convert.convert(
4✔
302
          Builtins.union(tmp, @current),
303
          from: "map",
304
          to:   "map <string, any>"
305
        )
306
      end
307

308
      merge_resource_aliases!
72✔
309
      generalCompat # compatibility to new language,keyboard and timezone (SL10.1)
72✔
310
      @current = Yast::ProfileHash.new(@current)
72✔
311
      softwareCompat
72✔
312
      log.info "Current Profile = #{@current}"
72✔
313

314
      nil
315
    end
316

317
    # Prepare Profile for saving and remove empty data structs
318
    # This is mainly for editing profile when there is some parts we do not write ourself
319
    # For creating new one from given set of modules see {#create}
320
    #
321
    # @param target [Symbol] How much information to include in the profile (:default, :compact)
322
    # @return [void]
323
    def Prepare(target: :default)
1✔
324
      return if !@prepare
10✔
325

326
      Popup.ShowFeedback(
9✔
327
        _("Collecting configuration data..."),
328
        _("This may take a while")
329
      )
330

331
      edit_profile(target: target)
9✔
332

333
      Popup.ClearFeedback
9✔
334
      @prepare = false
9✔
335
      nil
336
    end
337

338
    # Sets Profile#current to exported values created from given set of modules
339
    # @param target [Symbol] How much information to include in the profile (:default, :compact)
340
    # @return [Hash] value set to Profile#current
341
    def create(modules, target: :default)
1✔
342
      Popup.Feedback(
6✔
343
        _("Collecting configuration data..."),
344
        _("This may take a while")
345
      ) do
346
        @current = {}
6✔
347
        edit_profile(modules, target: target)
6✔
348
      end
349

350
      @current
6✔
351
    end
352

353
    # Reset profile to initial status
354
    # @return [void]
355
    def Reset
1✔
356
      Builtins.y2milestone("Resetting profile contents")
9✔
357
      @current = Yast::ProfileHash.new
9✔
358
      nil
359
    end
360

361
    # Save YCP data into XML
362
    # @param  file [String] path to file
363
    # @return [Boolean] true on success
364
    def Save(file)
1✔
365
      Prepare()
2✔
366
      Builtins.y2debug("Saving data (%1) to XML file %2", @current, file)
2✔
367
      XML.YCPToXMLFile(:profile, @current, file)
2✔
368

369
      if AutoinstConfig.ProfileEncrypted
1✔
370
        if [nil, ""].include?(AutoinstConfig.ProfilePassword)
×
371
          password = ::UI::PasswordDialog.new(
×
372
            _("Password for encrypted AutoYaST profile"), confirm: true
373
          ).run
374
          return false unless password
×
375

376
          AutoinstConfig.ProfilePassword = password
×
377
        end
378
        dir = SCR.Read(path(".target.tmpdir"))
×
379
        target_file = File.join(dir, "encrypted_autoyast.xml")
×
380
        GPG.encrypt_symmetric(file, target_file, AutoinstConfig.ProfilePassword)
×
381
        ::FileUtils.mv(target_file, file)
×
382
      end
383

384
      true
1✔
385
    rescue XMLSerializationError => e
386
      log.error "Failed to serialize objects: #{e.inspect}"
1✔
387
      false
1✔
388
    end
389

390
    # Save sections of current profile to separate files
391
    #
392
    # @param [String] dir - directory to store section xml files in
393
    # @return [Hash<String,String>] returns map with section name and respective file where
394
    #   it is serialized
395
    def SaveSingleSections(dir)
1✔
396
      Prepare()
2✔
397
      Builtins.y2milestone("Saving data (%1) to XML single files", @current)
2✔
398
      sectionFiles = {}
2✔
399
      Builtins.foreach(@current) do |sectionName, section|
2✔
400
        sectionFileName = Ops.add(
2✔
401
          Ops.add(Ops.add(dir, "/"), sectionName),
402
          ".xml"
403
        )
404
        tmpProfile = { sectionName => section }
2✔
405
        begin
406
          XML.YCPToXMLFile(:profile, tmpProfile, sectionFileName)
2✔
407
          Builtins.y2milestone(
1✔
408
            "Wrote section %1 to file %2",
409
            sectionName,
410
            sectionFileName
411
          )
412
          sectionFiles = Builtins.add(
1✔
413
            sectionFiles,
414
            sectionName,
415
            sectionFileName
416
          )
417
        rescue XMLSerializationError => e
418
          log.error "Could not write section #{sectionName} to file #{sectionFileName}:" \
1✔
419
                    "#{e.inspect}"
420
        end
421
      end
422
      deep_copy(sectionFiles)
2✔
423
    end
424

425
    # Save the current data into a file to be read after a reboot.
426
    # @param parsedControlFile [Hash] Data from control file
427
    # @return  true on success
428
    # @see #Restore()
429
    def SaveProfileStructure(parsedControlFile)
1✔
430
      Builtins.y2milestone("Saving control file in YCP format")
×
431
      SCR.Write(path(".target.ycp"), parsedControlFile, @current)
×
432
    end
433

434
    # Read YCP data as the control file
435
    # @param parsedControlFile [String] path of the ycp file
436
    # @return [Boolean] false when the file is empty or missing; true otherwise
437
    def ReadProfileStructure(parsedControlFile)
1✔
438
      contents = Convert.convert(
6✔
439
        SCR.Read(path(".target.ycp"), [parsedControlFile, {}]),
440
        from: "any",
441
        to:   "map <string, any>"
442
      )
443
      if contents == {}
6✔
444
        @current = Yast::ProfileHash.new(contents)
4✔
445
        return false
4✔
446
      else
447
        Import(contents)
2✔
448
      end
449

450
      true
2✔
451
    end
452

453
    # General compatibility issues
454
    # @param current [Hash] current profile
455
    # @return [Hash] converted profile
456
    def Compat(current)
1✔
NEW
457
      current = deep_copy(current)
×
458
      # scripts
NEW
459
      if Builtins.haskey(current, "pre-scripts") ||
×
460
          Builtins.haskey(current, "post-scripts") ||
461
          Builtins.haskey(current, "chroot-scripts")
NEW
462
        pre = Ops.get_list(current, "pre-scripts", [])
×
NEW
463
        post = Ops.get_list(current, "post-scripts", [])
×
NEW
464
        chroot = Ops.get_list(current, "chroot-scripts", [])
×
465
        scripts = {
466
          "pre-scripts"    => pre,
×
467
          "post-scripts"   => post,
468
          "chroot-scripts" => chroot
469
        }
NEW
470
        current = Builtins.remove(current, "pre-scripts")
×
NEW
471
        current = Builtins.remove(current, "post-scripts")
×
NEW
472
        current = Builtins.remove(current, "chroot-scripts")
×
473

NEW
474
        Ops.set(current, "scripts", scripts)
×
475
      end
476

477
      # general
478
      old = false
×
479

NEW
480
      general_options = Ops.get_map(current, "general", {})
×
NEW
481
      security = Ops.get_map(current, "security", {})
×
NEW
482
      report = Ops.get_map(current, "report", {})
×
483

484
      Builtins.foreach(general_options) do |k, v|
×
NEW
485
        if k == "encryption_method" || (["keyboard", "timezone"].include?(k) && Ops.is_string?(v))
×
486
          old = true
×
487
        end
488
      end
489

490
      new_general = {}
×
491

492
      if old
×
493
        Builtins.y2milestone("Old format, converting.....")
×
494
        Ops.set(
×
495
          new_general,
496
          "language",
497
          Ops.get_string(general_options, "language", "")
498
        )
499
        keyboard = {}
×
500
        Ops.set(
×
501
          keyboard,
502
          "keymap",
503
          Ops.get_string(general_options, "keyboard", "")
504
        )
505
        Ops.set(new_general, "keyboard", keyboard)
×
506

507
        clock = {}
×
508
        Ops.set(
×
509
          clock,
510
          "timezone",
511
          Ops.get_string(general_options, "timezone", "")
512
        )
NEW
513
        case Ops.get_string(general_options, "hwclock", "")
×
514
        when "localtime"
515
          Ops.set(clock, "hwclock", "localtime")
×
516
        when "GMT"
517
          Ops.set(clock, "hwclock", "GMT")
×
518
        end
519
        Ops.set(new_general, "clock", clock)
×
520

521
        mode = {}
×
522
        if Builtins.haskey(general_options, "reboot")
×
523
          Ops.set(
×
524
            mode,
525
            "reboot",
526
            Ops.get_boolean(general_options, "reboot", false)
527
          )
528
        end
529
        if Builtins.haskey(report, "confirm")
×
530
          Ops.set(mode, "confirm", Ops.get_boolean(report, "confirm", false))
×
531
          report = Builtins.remove(report, "confirm")
×
532
        end
533
        Ops.set(new_general, "mode", mode)
×
534

535
        if Builtins.haskey(general_options, "encryption_method")
×
536
          Ops.set(
×
537
            security,
538
            "encryption",
539
            Ops.get_string(general_options, "encryption_method", "")
540
          )
541
        end
542

NEW
543
        net = Ops.get_map(current, "networking", {})
×
544
        ifaces = Ops.get_list(net, "interfaces", [])
×
545

546
        newifaces = Builtins.maplist(ifaces) do |iface|
×
547
          newiface = Builtins.mapmap(iface) do |k, v|
×
548
            { Builtins.tolower(k) => v }
×
549
          end
550
          deep_copy(newiface)
×
551
        end
552

553
        Ops.set(net, "interfaces", newifaces)
×
554

NEW
555
        Ops.set(current, "general", new_general)
×
NEW
556
        Ops.set(current, "report", report)
×
NEW
557
        Ops.set(current, "security", security)
×
NEW
558
        Ops.set(current, "networking", net)
×
559
      end
560

NEW
561
      deep_copy(current)
×
562
    end
563

564
    # Read XML into  YCP data
565
    # @param file [String] path to file
566
    # @return [Boolean]
567
    def ReadXML(file)
1✔
568
      if GPG.encrypted_symmetric?(file)
34✔
569
        AutoinstConfig.ProfileEncrypted = true
2✔
570
        label = _("Encrypted AutoYaST profile.")
2✔
571

572
        begin
573
          if AutoinstConfig.ProfilePassword.empty?
2✔
574
            pwd = ::UI::PasswordDialog.new(label).run
1✔
575
            return false unless pwd
1✔
576
          else
577
            pwd = AutoinstConfig.ProfilePassword
1✔
578
          end
579

580
          content = GPG.decrypt_symmetric(file, pwd)
2✔
581
          AutoinstConfig.ProfilePassword = pwd
2✔
582
        rescue GPGFailed => e
583
          res = Yast2::Popup.show(_("Decryption of profile failed."),
×
584
            details: e.message, headline: :error, buttons: :continue_cancel)
585
          if res == :continue
×
586
            retry
×
587
          else
588
            return false
×
589
          end
590
        end
591
      else
592
        content = File.read(file)
32✔
593
      end
594
      @current = XML.XMLToYCPString(content)
34✔
595

596
      # FIXME: rethink and check for sanity of that!
597
      # save decrypted profile for modifying pre-scripts
598
      if Stage.initial
31✔
599
        SCR.Write(
21✔
600
          path(".target.string"),
601
          file,
602
          content
603
        )
604
      end
605

606
      Import(@current)
31✔
607
      true
31✔
608
    rescue Yast::XMLDeserializationError => e
609
      # autoyast has read the autoyast configuration file but something went wrong
610
      message = _(
3✔
611
        "The XML parser reported an error while parsing the autoyast profile. " \
612
        "The error message is:\n"
613
      )
614
      message += e.message
3✔
615
      log.info "xml parsing error #{e.inspect}"
3✔
616
      Yast2::Popup.show(message, headline: :error)
3✔
617
      false
3✔
618
    end
619

620
    # Returns a profile merging the given value into the specified path
621
    #
622
    # The path can be a String or a Installation::AutoinstProfile::ElementPath
623
    # object. Although the real work is performed by {setElementByList}, it is
624
    # usually preferred to use this method as it takes care of handling the
625
    # path.
626
    #
627
    # @example Set a value using a XPath-like path
628
    #   path = "//a/b"
629
    #   set_element_by_path(path, 1, {}) #=> { "a" => { "b" => 1 } }
630
    #
631
    # @example Set a value using an ask-list style path
632
    #   path = "users,0,username"
633
    #   set_element_by_path(path, "root", {}) #=> { "users" => [{"username" => "root"}] }
634
    #
635
    # @param path [ElementPath,String] Profile path or string representing the path
636
    # @param value [Object] Value to write
637
    # @param profile [Hash] Initial profile
638
    # @return [Hash] Modified profile
639
    def set_element_by_path(path, value, profile)
1✔
640
      profile_path =
641
        if path.is_a?(::String)
4✔
642
          ::Installation::AutoinstProfile::ElementPath.from_string(path)
3✔
643
        else
644
          path
1✔
645
        end
646
      setElementByList(profile_path.to_a, value, profile)
4✔
647
    end
648

649
    # Returns a profile merging the given value into the specified path
650
    #
651
    # The given profile is not modified.
652
    #
653
    # This method is a replacement for this YCP code:
654
    #      list<any> l = [ "key1",0,"key3" ];
655
    #      m[ l ] = v;
656
    #
657
    # @example Set a value
658
    #   path = ["a", "b"]
659
    #   setElementByList(path, 1, {}) #=> { "a" => { "b" => 1 } }
660
    #
661
    # @example Add an element to an array
662
    #   path = ["users", 0, "username"]
663
    #   setElementByList(path, "root", {}) #=> { "users" => [{"username" => "root"}] }
664
    #
665
    # @example Beware of the gaps!
666
    #   path = ["users", 1, "username"]
667
    #   setElementByList(path, "root", {}) #=> { "users" => [nil, {"username" => "root"}] }
668
    #
669
    # @param path [Array<String,Integer>] Element's path
670
    # @param value [Object] Value to write
671
    # @param profile [Hash] Initial profile
672
    # @return [Hash] Modified profile
673
    def setElementByList(path, value, profile)
1✔
674
      merge_element_by_list(path, value, profile)
7✔
675
    end
676

677
    # @deprecated Unused, removed
678
    def checkProfile
1✔
679
      log.warn("Profile.checkProfile() is obsolete, do not use it")
×
680
      log.warn("Called from #{caller(1).first}")
×
681
    end
682

683
    # Removes the given sections from the profile
684
    #
685
    # @param sections [String,Array<String>] Section names.
686
    # @return [Hash] The profile without the removed sections.
687
    def remove_sections(sections)
1✔
688
      keys_to_delete = Array(sections)
16✔
689
      @current.delete_if { |k, _v| keys_to_delete.include?(k) }
39✔
690
    end
691

692
    # Returns a list of packages which have to be installed
693
    # in order to run a second stage at all.
694
    #
695
    # @return [Array<String>] package list
696
    def needed_second_stage_packages
1✔
697
      ret = ["autoyast2-installation"]
8✔
698

699
      # without autoyast2, <files ...> does not work
700
      ret << "autoyast2" if !(@current.keys & AUTOYAST_CLIENTS).empty?
8✔
701
      ret
8✔
702
    end
703

704
    # @!attribute current
705
    #   @return [Hash<String, Object>] current working profile
706
    publish variable: :current, type: "map <string, any>"
1✔
707
    publish variable: :changed, type: "boolean"
1✔
708
    publish variable: :prepare, type: "boolean"
1✔
709
    publish function: :Import, type: "void (map <string, any>)"
1✔
710
    publish function: :Prepare, type: "void ()"
1✔
711
    publish function: :Reset, type: "void ()"
1✔
712
    publish function: :Save, type: "boolean (string)"
1✔
713
    publish function: :SaveSingleSections, type: "map <string, string> (string)"
1✔
714
    publish function: :SaveProfileStructure, type: "boolean (string)"
1✔
715
    publish function: :ReadProfileStructure, type: "boolean (string)"
1✔
716
    publish function: :ReadXML, type: "boolean (string)"
1✔
717
    publish function: :setElementByList, type: "map <string, any> (list, any, map <string, any>)"
1✔
718
    publish function: :checkProfile, type: "void ()"
1✔
719
    publish function: :needed_second_stage_packages, type: "list <string> ()"
1✔
720

721
  private
1✔
722

723
    REBOOT_SCRIPT = {
724
      "filename" => "zzz_reboot",
1✔
725
      "source"   => "shutdown -r now"
726
    }.freeze
727

728
    HALT_SCRIPT = {
729
      "filename" => "zzz_halt",
1✔
730
      "source"   => "shutdown -h now"
731
    }.freeze
732

733
    def add_autoyast_packages
1✔
734
      @current["software"]["packages"] = @current["software"].fetch_as_array("packages")
5✔
735
      @current["software"]["packages"] << needed_second_stage_packages
5✔
736
      @current["software"]["packages"].flatten!.uniq!
5✔
737
    end
738

739
  protected
1✔
740

741
    # Merge resource aliases in the profile
742
    #
743
    # When a resource is aliased, the configuration with the aliased name will
744
    # be renamed to the new name. For example, if we have a
745
    # services-manager.desktop file containing
746
    # X-SuSE-YaST-AutoInstResourceAliases=runlevel, if a "runlevel" key is found
747
    # in the profile, it will be renamed to "services-manager".
748
    #
749
    # The rename won't take place if a "services-manager" resource already exists.
750
    #
751
    # @see merge_aliases_map
752
    def merge_resource_aliases!
1✔
753
      reg = Y2Autoinstallation::Entries::Registry.instance
72✔
754
      alias_map = reg.descriptions.each_with_object({}) do |d, r|
72✔
755
        d.aliases.each { |a| r[a] = d.resource_name || d.name }
1,227✔
756
      end
757
      alias_map.each do |alias_name, resource_name|
72✔
758
        aliased_config = current.delete(alias_name)
67✔
759
        next if aliased_config.nil? || current.key?(resource_name)
67✔
760

761
        current[resource_name] = aliased_config
3✔
762
      end
763
    end
764

765
    # Edits profile for given modules. If nil is passed, it used GetModfied method.
766
    def edit_profile(modules = nil, target: :default)
1✔
767
      registry = Y2Autoinstallation::Entries::Registry.instance
15✔
768
      registry.descriptions.each do |description|
15✔
769
        #
770
        # Set resource name, if not using default value
771
        #
772
        resource = description.resource_name
15✔
773
        tomerge = description.managed_keys
15✔
774
        module_auto = description.client_name
15✔
775
        export = if modules
15✔
776
          modules.include?(resource) || modules.include?(description.name)
6✔
777
        else
778
          WFM.CallFunction(module_auto, ["GetModified"])
9✔
779
        end
780
        next unless export
15✔
781

782
        resource_data = WFM.CallFunction(module_auto, ["Export", { "target" => target.to_s }])
13✔
783

784
        if tomerge.size < 2
13✔
785
          s = (resource_data || {}).size
9✔
786
          if s > 0
9✔
787
            @current[resource] = resource_data
6✔
788
          else
789
            @current.delete(resource)
3✔
790
          end
791
        else
792
          tomerge.each do |res|
4✔
793
            value = resource_data[res]
8✔
794
            if !value
8✔
795
              log.warn "key #{res} expected to be exported from #{resource}"
2✔
796
              next
2✔
797
            end
798
            # FIXME: no deleting for merged keys, is it correct?
799
            @current[res] = value unless value.empty?
6✔
800
          end
801
        end
802
      end
803
    end
804

805
    # @see setElementByList
806
    def merge_element_by_list(path, value, profile)
1✔
807
      current, *remaining_path = path
21✔
808
      current_value =
809
        if remaining_path.empty?
21✔
810
          value
7✔
811
        elsif remaining_path.first.is_a?(::String)
14✔
812
          merge_element_by_list(remaining_path, value, profile[current] || Yast::ProfileHash.new)
8✔
813
        else
814
          merge_element_by_list(remaining_path, value, profile[current] || [])
6✔
815
        end
816
      log.debug("Setting #{current} to #{current_value.inspect}")
21✔
817
      profile[current] = current_value
21✔
818
      profile
21✔
819
    end
820
  end
821

822
  Profile = ProfileClass.new
1✔
823
  Profile.main
1✔
824
end
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc