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

yast / yast-yast2 / 13440235285

20 Feb 2025 04:40PM UTC coverage: 41.869% (-0.02%) from 41.889%
13440235285

push

github

web-flow
Merge pull request #1316 from yast/agama_kernel_conf

Respect Agama kernel parameters

2 of 4 new or added lines in 1 file covered. (50.0%)

265 existing lines in 40 files now uncovered.

12605 of 30106 relevant lines covered (41.87%)

10.76 hits per line

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

56.61
/library/control/src/modules/WorkflowManager.rb
1
# ***************************************************************************
2
#
3
# Copyright (c) 2002 - 2012 Novell, Inc.
4
# All Rights Reserved.
5
#
6
# This program is free software; you can redistribute it and/or
7
# modify it under the terms of version 2 of the GNU General Public License as
8
# published by the Free Software Foundation.
9
#
10
# This program is distributed in the hope that it will be useful,
11
# but WITHOUT ANY WARRANTY; without even the implied warranty of
12
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.   See the
13
# GNU General Public License for more details.
14
#
15
# You should have received a copy of the GNU General Public License
16
# along with this program; if not, contact Novell, Inc.
17
#
18
# To contact Novell about this file by physical or electronic mail,
19
# you may find current contact information at www.novell.com
20
#
21
# ***************************************************************************
22
# File:  modules/WorkflowManager.rb
23
# Package:  yast2
24
# Summary:  Provides API for configuring workflows
25
# Authors:  Lukas Ocilka <locilka@suse.cz>
26
#
27
# Provides API for managing and configuring installation and
28
# configuration workflow.
29
#
30
# Module was created as a solution for
31
# FATE #129: Framework for pattern based Installation/Deployment
32
#
33
# Module unifies Add-Ons and Patterns modifying the workflow.
34
#
35
require "yast"
1✔
36
require "yast2/control_log_dir_rotator"
1✔
37

38
require "packages/package_downloader"
1✔
39
require "packages/package_extractor"
1✔
40
require "y2packager/resolvable"
1✔
41

42
module Yast
1✔
43
  class WorkflowManagerClass < Module
1✔
44
    include Yast::Logger
1✔
45

46
    def main
1✔
47
      Yast.import "UI"
5✔
48
      Yast.import "Pkg"
5✔
49

50
      textdomain "base"
5✔
51

52
      Yast.import "ProductControl"
5✔
53
      Yast.import "ProductFeatures"
5✔
54

55
      Yast.import "Label"
5✔
56
      Yast.import "Wizard"
5✔
57
      Yast.import "Directory"
5✔
58
      Yast.import "FileUtils"
5✔
59
      Yast.import "Stage"
5✔
60
      Yast.import "String"
5✔
61
      Yast.import "XML"
5✔
62
      Yast.import "Report"
5✔
63
      Yast.import "Mode"
5✔
64

65
      #
66
      #    This API uses some new terms that need to be explained:
67
      #
68
      #    * Workflow Store
69
      #      - Kind of database of installation or configuration workflows
70
      #
71
      #    * Base Workflow
72
      #      - The initial workflow defined by the base product
73
      #      - In case of running system, this will be probably empty
74
      #
75
      #    * Additional Workflow
76
      #      - Any workflow defined by Add-On or Pattern in installation
77
      #        or Pattern in running system
78
      #
79
      #    * Final Workflow
80
      #      - Workflow that contains the base workflow modified by all
81
      #        additional workflows
82
      #
83

84
      # Base Workflow Store
85
      @wkf_initial_workflows = []
5✔
86
      @wkf_initial_proposals = []
5✔
87
      @wkf_initial_inst_finish = []
5✔
88
      @wkf_initial_clone_modules = []
5✔
89
      @wkf_initial_system_roles = []
5✔
90

91
      @wkf_initial_product_features = {}
5✔
92

93
      # Additional inst_finish settings defined by additional control files.
94
      # They are always empty at the begining.
95
      @additional_finish_steps_before_chroot = []
5✔
96
      @additional_finish_steps_after_chroot = []
5✔
97
      @additional_finish_steps_before_umount = []
5✔
98

99
      # FATE #305578: Add-On Product Requiring Registration
100
      # $[ "workflow filename" : (boolean) require_registration ]
101
      @workflows_requiring_registration = []
5✔
102

103
      @workflows_to_sources = {}
5✔
104

105
      @base_workflow_stored = false
5✔
106

107
      # Contains all currently workflows added to the Workflow Store
108
      @used_workflows = []
5✔
109

110
      # Some workflow changes need merging
111
      @unmerged_changes = false
5✔
112

113
      # Have system proposals already been prepared for merging?
114
      @system_proposals_prepared = false
5✔
115

116
      # Have system workflows already been prepared for merging?
117
      @system_workflows_prepared = false
5✔
118

119
      @control_files_dir = "additional-control-files"
5✔
120

121
      # Merge counter used for logging
122
      @merge_counter = 0
5✔
123

124
      # base product that got its workflow merged
125
      # @see #merge_product_workflow
126
      self.merged_base_product = nil
5✔
127

128
      self.merged_modules_extensions = []
5✔
129
    end
130

131
    # Returns list of additional inst_finish steps requested by
132
    # additional workflows.
133
    #
134
    # @param [String] which_steps (type) of finish ("before_chroot", "after_chroot" or "before_umount")
135
    # @return [Array<String>] steps to be called ...see which_steps parameter
136
    def GetAdditionalFinishSteps(which_steps)
1✔
137
      ret = case which_steps
×
138
      when "before_chroot"
139
        @additional_finish_steps_before_chroot
×
140
      when "after_chroot"
141
        @additional_finish_steps_after_chroot
×
142
      when "before_umount"
143
        @additional_finish_steps_before_umount
×
144
      else
145
        Builtins.y2error("Unknown FinishSteps type: %1", which_steps)
×
146
        nil
×
147
      end
148

149
      deep_copy(ret)
×
150
    end
151

152
    # Stores the current ProductControl settings as the initial settings.
153
    # These settings are: workflows, proposals, inst_finish, and clone_modules.
154
    #
155
    # @param [Boolean] force storing even if it was already stored, in most cases, it should be 'false'
156
    def SetBaseWorkflow(force)
1✔
157
      if @base_workflow_stored && !force
×
158
        Builtins.y2milestone("Base Workflow has been already set")
×
159
        return
×
160
      end
161

162
      @wkf_initial_product_features = ProductFeatures.Export
×
163

164
      @wkf_initial_workflows = deep_copy(ProductControl.workflows)
×
165
      @wkf_initial_proposals = deep_copy(ProductControl.proposals)
×
166
      @wkf_initial_inst_finish = deep_copy(ProductControl.inst_finish)
×
167
      @wkf_initial_clone_modules = deep_copy(ProductControl.clone_modules)
×
168
      @wkf_initial_system_roles = deep_copy(ProductControl.system_roles)
×
169

170
      @additional_finish_steps_before_chroot = []
×
171
      @additional_finish_steps_after_chroot = []
×
172
      @additional_finish_steps_before_umount = []
×
173

174
      @base_workflow_stored = true
×
175

UNCOV
176
      nil
×
177
    end
178

179
    # Check all proposals, split those ones which have multiple modes or
180
    # architectures or stages into multiple proposals.
181
    #
182
    # @param list <map> current proposals
183
    # @return [Array<Hash>] updated proposals
184
    #
185
    #
186
    # **Structure:**
187
    #
188
    #
189
    #       Input: [
190
    #         $["label":"Example", "name":"example","proposal_modules":["one","two"],"stage":"initial,firstboot"]
191
    #       ]
192
    #       Output: [
193
    #         $["label":"Example", "name":"example","proposal_modules":["one","two"],"stage":"initial"]
194
    #         $["label":"Example", "name":"example","proposal_modules":["one","two"],"stage":"firstboot"]
195
    #       ]
196
    def PrepareProposals(proposals)
1✔
197
      proposals = deep_copy(proposals)
4✔
198
      new_proposals = []
4✔
199

200
      # Going through all proposals
201
      Builtins.foreach(proposals) do |one_proposal|
4✔
202
        mode = Ops.get_string(one_proposal, "mode", "")
9✔
203
        modes = Builtins.splitstring(mode, ",")
9✔
204
        modes = [""] if Builtins.size(modes) == 0
9✔
205
        # Going through all modes in proposal
206
        Builtins.foreach(modes) do |one_mode|
9✔
207
          mp = deep_copy(one_proposal)
11✔
208
          Ops.set(mp, "mode", one_mode)
11✔
209
          arch = Ops.get_string(one_proposal, "archs", "")
11✔
210
          archs = Builtins.splitstring(arch, ",")
11✔
211
          archs = [""] if Builtins.size(archs) == 0
11✔
212
          # Going through all architectures
213
          Builtins.foreach(archs) do |one_arch|
11✔
214
            amp = deep_copy(mp)
11✔
215
            Ops.set(amp, "archs", one_arch)
11✔
216
            stage = Ops.get_string(amp, "stage", "")
11✔
217
            stages = Builtins.splitstring(stage, ",")
11✔
218
            stages = [""] if Builtins.size(stages) == 0
11✔
219
            # Going through all stages
220
            Builtins.foreach(stages) do |one_stage|
11✔
221
              single_proposal = deep_copy(amp)
11✔
222
              Ops.set(single_proposal, "stage", one_stage)
11✔
223
              new_proposals = Builtins.add(new_proposals, single_proposal)
11✔
224
            end
225
          end
226
        end
227
      end
228

229
      deep_copy(new_proposals)
4✔
230
    end
231

232
    # Check all proposals, split those ones which have multiple modes or
233
    # architectures or stages into multiple proposals.
234
    # Works with base product proposals.
235
    def PrepareSystemProposals
1✔
236
      return if @system_proposals_prepared
3✔
237

238
      ProductControl.proposals = PrepareProposals(ProductControl.proposals)
1✔
239
      @system_proposals_prepared = true
1✔
240

241
      nil
1✔
242
    end
243

244
    # Check all workflows, split those ones which have multiple modes or
245
    # architectures or stages into multiple workflows
246
    # @param [Array<Hash>] workflows
247
    # @return [Array<Hash>] updated workflows
248
    def PrepareWorkflows(workflows)
1✔
249
      workflows = deep_copy(workflows)
4✔
250
      new_workflows = []
4✔
251

252
      # Going through all workflows
253
      Builtins.foreach(workflows) do |one_workflow|
4✔
254
        mode = Ops.get_string(one_workflow, "mode", "")
9✔
255
        modes = Builtins.splitstring(mode, ",")
9✔
256
        modes = [""] if Builtins.size(modes) == 0
9✔
257
        # Going through all modes
258
        Builtins.foreach(modes) do |one_mode|
9✔
259
          mw = deep_copy(one_workflow)
9✔
260
          Ops.set(mw, "mode", one_mode)
9✔
261
          Ops.set(mw, "defaults", Ops.get_map(mw, "defaults", {}))
9✔
262
          arch = Ops.get_string(mw, ["defaults", "archs"], "")
9✔
263
          archs = Builtins.splitstring(arch, ",")
9✔
264
          archs = [""] if Builtins.size(archs) == 0
9✔
265
          # Going through all architercures
266
          Builtins.foreach(archs) do |one_arch|
9✔
267
            amw = deep_copy(mw)
9✔
268
            Ops.set(amw, ["defaults", "archs"], one_arch)
9✔
269
            stage = Ops.get_string(amw, "stage", "")
9✔
270
            stages = Builtins.splitstring(stage, ",")
9✔
271
            stages = [""] if Builtins.size(stages) == 0
9✔
272
            # Going through all stages
273
            Builtins.foreach(stages) do |one_stage|
9✔
274
              single_workflow = deep_copy(amw)
9✔
275
              Ops.set(single_workflow, "stage", one_stage)
9✔
276
              new_workflows = Builtins.add(new_workflows, single_workflow)
9✔
277
            end
278
          end
279
        end
280
      end
281

282
      deep_copy(new_workflows)
4✔
283
    end
284

285
    # Check all workflows, split those ones which have multiple modes or
286
    # architectures or stages into multiple worlflows.
287
    # Works with base product workflows.
288
    def PrepareSystemWorkflows
1✔
289
      return if @system_workflows_prepared
3✔
290

291
      ProductControl.workflows = PrepareWorkflows(ProductControl.workflows)
1✔
292
      @system_workflows_prepared = true
1✔
293

294
      nil
1✔
295
    end
296

297
    # Fills the workflow with initial settings to start merging from scratch.
298
    # Used workflows mustn't be cleared automatically, merging would fail!
299
    def FillUpInitialWorkflowSettings
1✔
300
      if !@base_workflow_stored
×
301
        Builtins.y2error(
×
302
          "Base Workflow has never been stored, you should have called SetBaseWorkflow() before!"
303
        )
304
      end
305

306
      ProductFeatures.Import(@wkf_initial_product_features)
×
307

308
      ProductControl.workflows = deep_copy(@wkf_initial_workflows)
×
309
      ProductControl.proposals = deep_copy(@wkf_initial_proposals)
×
310
      ProductControl.inst_finish = deep_copy(@wkf_initial_inst_finish)
×
311
      ProductControl.clone_modules = deep_copy(@wkf_initial_clone_modules)
×
312
      ProductControl.system_roles = deep_copy(@wkf_initial_system_roles)
×
313

314
      @additional_finish_steps_before_chroot = []
×
315
      @additional_finish_steps_after_chroot = []
×
316
      @additional_finish_steps_before_umount = []
×
317

318
      @workflows_requiring_registration = []
×
319
      @workflows_to_sources = {}
×
320

321
      # reset internal variable to force the Prepare... function
322
      @system_proposals_prepared = false
×
323
      PrepareSystemProposals()
×
324

325
      # reset internal variable to force the Prepare... function
326
      @system_workflows_prepared = false
×
327
      PrepareSystemWorkflows()
×
328

UNCOV
329
      nil
×
330
    end
331

332
    # Resets the Workflow (and proposals) to use the base workflow. It must be stored.
333
    # Clears also all additional workflows.
334
    def ResetWorkflow
1✔
335
      FillUpInitialWorkflowSettings()
×
336
      @used_workflows = []
×
337

UNCOV
338
      nil
×
339
    end
340

341
    # Returns the current (default) directory where workflows are stored in.
342
    def GetWorkflowDirectory
1✔
343
      Builtins.sformat("%1/%2", Directory.tmpdir, @control_files_dir)
×
344
    end
345

346
    # Creates path to a control file from parameters. For add-on products,
347
    # the 'ident' parameter is empty.
348
    #
349
    # @param [Fixnum] src_id with source ID
350
    # @param [String] ident with pattern name (or another unique identification), empty for Add-Ons
351
    # @return [String] path to a control file based on src_id and ident params
352
    def GenerateAdditionalControlFilePath(src_id, ident)
1✔
353
      # special handling for Add-Ons (they have no special ident)
354
      ident = "__AddOnProduct-ControlFile__" if ident == ""
×
355

356
      Builtins.sformat("%1/%2:%3.xml", GetWorkflowDirectory(), src_id, ident)
×
357
    end
358

359
    # Stores the workflow file to a cache
360
    #
361
    # @param [String] file_from filename
362
    # @param [String] file_to filename
363
    # @return [String] final filename
364
    def StoreWorkflowFile(file_from, file_to)
1✔
365
      if file_from.nil? || file_from == "" || file_to.nil? || file_to == ""
×
366
        Builtins.y2error("Cannot copy '%1' to '%2'", file_from, file_to)
×
367
        return nil
×
368
      end
369

370
      # Return nil if cannot copy
371
      file_location = nil
×
372

373
      Builtins.y2milestone(
×
374
        "Copying workflow from '%1' to '%2'",
375
        file_from,
376
        file_to
377
      )
378
      cmd = Convert.to_map(
×
379
        SCR.Execute(
380
          path(".target.bash_output"),
381
          Builtins.sformat(
382
            "\n" \
383
            "/bin/mkdir -p '%1';\n" \
384
            "/bin/cp -v '%2' '%3';\n",
385
            String.Quote(GetWorkflowDirectory()),
386
            String.Quote(file_from),
387
            String.Quote(file_to)
388
          )
389
        )
390
      )
391

392
      # successfully copied
393
      if Ops.get_integer(cmd, "exit", -1) == 0
×
394
        file_location = file_to
×
395
      else
396
        Builtins.y2error("Error occurred while copying control file: %1", cmd)
×
397

398
        # Not in installation, try to skip the error
399
        if !Stage.initial && FileUtils.Exists(file_from)
×
400
          Builtins.y2milestone("Using fallback file %1", file_from)
×
401
          file_location = file_from
×
402
        end
403
      end
404

405
      file_location
×
406
    end
407

408
    # Download and extract the control file (installation.xml) from the add-on
409
    # repository.
410
    #
411
    # @param source [String, Fixnum] source where to get control file. It can be fixnum for
412
    #   addon type or package name for package type
413
    # @return [String, nil] path to downloaded installation.xml file or nil
414
    #   or nil when no workflow is defined or the workflow package is missing
415
    def control_file(source)
1✔
416
      package = case source
21✔
417
      when ::Integer
418
        product = find_product(source)
21✔
419
        return nil unless product&.product_package
21✔
420

421
        product_package = product.product_package
19✔
422

423
        # the dependencies are bound to the product's -release package
424
        release_package = Y2Packager::Resolvable.find(kind: :package, name: product_package).first
19✔
425

426
        # find the package name with installer update in its Provide dependencies
427
        control_file_package = find_control_package(release_package)
19✔
428
        return nil unless control_file_package
19✔
429

430
        control_file_package
16✔
431
      when ::String
432
        source
×
433
      else
434
        raise ArgumentError, "Invalid argument source #{source.inspect}"
×
435
      end
436

437
      # get the repository ID of the package
438
      src = package_repository(package)
16✔
439
      return nil unless src
16✔
440

441
      # ensure the previous content is removed, the src should avoid
442
      # collisions but rather be safe...
443
      dir = addon_control_dir(src, cleanup: true)
15✔
444
      fetch_package(src, package, dir)
15✔
445

446
      path = control_file_at_dir(dir)
11✔
447
      return nil unless File.exist?(path)
11✔
448

449
      log.info("installation.xml path: #{path}")
8✔
450
      path
8✔
451
    rescue Y2Packager::PackageFetchError
452
      # TRANSLATORS: an error message
453
      Report.Error(_("Downloading the installer extension package failed."))
4✔
454
      nil
4✔
455
    rescue Y2Packager::PackageExtractionError
456
      # TRANSLATORS: an error message
457
      Report.Error(_("Extracting the installer extension failed."))
×
458
      nil
×
459
    end
460

461
    # Create a temporary directory for storing the installer extension package content.
462
    # The directory is automatically removed at exit.
463
    # @param src_id [Fixnum] repository ID
464
    # @param cleanup [Boolean] remove the content if the directory already exists
465
    # @return [String] directory path
466
    def addon_control_dir(src_id, cleanup: false)
1✔
467
      # Directory.tmpdir is automatically removed at exit
468
      dir = File.join(Directory.tmpdir, "installer-extension-#{src_id}")
27✔
469
      ::FileUtils.remove_entry(dir) if cleanup && Dir.exist?(dir)
27✔
470
      ::FileUtils.mkdir_p(dir) unless Dir.exist?(dir)
27✔
471
      dir
27✔
472
    end
473

474
    # Path of the control file contained in the package that has been previously
475
    # extracted to the given directory
476
    #
477
    # @see #control_file
478
    #
479
    # @param dir [String] directory where the package has been extracted to
480
    # @return [String] name of the control file
481
    def control_file_at_dir(dir)
1✔
482
      # Lets first try FHS compliant path for a product package (fate#325482)
483
      path = find_control_file("#{dir}/usr/share/installation-products")
11✔
484

485
      # If nothing there, try FHS compliant path for a role package (bsc#1114573)
486
      path ||= find_control_file("#{dir}/usr/share/system-roles")
11✔
487

488
      # As last resort, use the default location at /installation.xml
489
      path ||= File.join(dir, "installation.xml")
11✔
490

491
      path
11✔
492
    end
493

494
    # Full name of the control file located directly in the given directory
495
    #
496
    # The content of the file is not verified to be compliant with the structure
497
    # of a control file, this method simply finds the (hopefully only) XML file
498
    # in the directory.
499
    #
500
    # @param dir [String] directory where the control file is expected to be
501
    # @return [String, nil] nil if there is no control file
502
    def find_control_file(dir)
1✔
503
      # sadly no glob escaping - https://bugs.ruby-lang.org/issues/8258
504
      # but as we generate directory, it should be ok
505
      files = Dir.glob("#{dir}/*.xml")
18✔
506

507
      log.error "More than one XML file in #{dir}: #{files.inspect}" if files.size > 1
18✔
508

509
      files.first
18✔
510
    end
511

512
    # Returns requested control filename. Parameter 'name' is ignored
513
    # for Add-Ons.
514
    #
515
    # @param [Symbol] type :addon or :package
516
    # @param [Fixnum] src_id with Source ID
517
    # @param [String] name with unique identification, ignored for addon
518
    # @return [String] path to already cached workflow file, control file is downloaded if not yet cached
519
    #   or nil if failed to get filename
520
    def GetCachedWorkflowFilename(type, src_id, name = "")
1✔
521
      if ![:package, :addon].include?(type)
×
522
        Builtins.y2error("Unknown workflow type: %1", type)
×
523
        return nil
×
524
      end
525

526
      disk_filename = GenerateAdditionalControlFilePath(src_id, name)
×
527

528
      # A cached copy exists
529
      if FileUtils.Exists(disk_filename)
×
530
        Builtins.y2milestone("Using cached file %1", disk_filename)
×
531
        disk_filename
×
532
        # Trying to get the file from source
533
      else
534
        Builtins.y2milestone("File %1 not cached", disk_filename)
×
535
        case type
×
536
        when :addon
537
          # using a file from source, works only for SUSE tags repositories
538
          use_filename = Pkg.SourceProvideDigestedFile(
×
539
            src_id,
540
            1,
541
            "/installation.xml",
542
            true
543
          )
544

545
          # The most generic way it to use the package referenced by the "installerextension()"
546
          # provides, this works with all repository types, including the RPM-MD repositories.
547
          use_filename ||= control_file(src_id)
×
548
        when :package
549
          use_filename = control_file(name)
×
550
        end
551

552
        # File exists?
553
        use_filename.nil? ? nil : StoreWorkflowFile(use_filename, disk_filename)
×
554
      end
555
    ensure
556
      # release the media accessors (close server connections/unmount disks)
557
      Pkg.SourceReleaseAll
×
558
    end
559

560
    # Stores new workflow (if such workflow exists) into the Worflow Store.
561
    #
562
    # @param [Symbol] type :addon or :package
563
    # @param intger src_id with source ID
564
    # @param [String] name with unique identification name of the object
565
    #        ("" for `addon, package name for :package)
566
    # @return [Boolean] whether successful (true also in case of no workflow file)
567
    #
568
    # @example
569
    #  AddWorkflow (`addon, 4, "");
570
    def AddWorkflow(type, src_id, name)
1✔
571
      Builtins.y2milestone(
×
572
        "Adding Workflow:  Type %1, ID %2, Name %3",
573
        type,
574
        src_id,
575
        name
576
      )
577
      if !Builtins.contains([:addon, :package], type)
×
578
        Builtins.y2error("Unknown workflow type: %1", type)
×
579
        return false
×
580
      end
581

582
      name = "" if type == :addon
×
583
      # new xml filename
584
      used_filename = GetCachedWorkflowFilename(type, src_id, name)
×
585

586
      if !used_filename.nil? && used_filename != ""
×
587
        @unmerged_changes = true
×
588

589
        @used_workflows = Builtins.add(@used_workflows, used_filename)
×
590
        Ops.set(@workflows_to_sources, used_filename, src_id)
×
591
      end
592

593
      true
×
594
    end
595

596
    # Removes workflow (if such workflow exists) from the Worflow Store.
597
    # Alose removes the cached file but in the installation.
598
    #
599
    # @param [Symbol] type :addon or :package
600
    # @param [Integer] src_id with source ID
601
    # @param [String] name with unique identification name of the object.
602
    #   For :addon it should be empty string
603
    #
604
    # @return [Boolean] whether successful (true also in case of no workflow file)
605
    #
606
    # @example
607
    #  RemoveWorkflow (:addon, 4, "");
608
    def RemoveWorkflow(type, src_id, name)
1✔
609
      Builtins.y2milestone(
×
610
        "Removing Workflow:  Type %1, ID %2, Name %3",
611
        type,
612
        src_id,
613
        name
614
      )
615
      if !Builtins.contains([:addon, :package], type)
×
616
        Builtins.y2error("Unknown workflow type: %1", type)
×
617
        return false
×
618
      end
619

620
      name = "" if type == :addon
×
621
      # cached xml file
622
      used_filename = GenerateAdditionalControlFilePath(src_id, name)
×
623

624
      if !used_filename.nil? && used_filename != ""
×
625
        @unmerged_changes = true
×
626

627
        @used_workflows = Builtins.filter(@used_workflows) do |one_workflow|
×
628
          one_workflow != used_filename
×
629
        end
630

631
        if Builtins.haskey(@workflows_to_sources, used_filename)
×
632
          @workflows_to_sources = Builtins.remove(
×
633
            @workflows_to_sources,
634
            used_filename
635
          )
636
        end
637

638
        if !Stage.initial && FileUtils.Exists(used_filename)
×
639
          Builtins.y2milestone(
×
640
            "Removing cached file '%1': %2",
641
            used_filename,
642
            SCR.Execute(path(".target.remove"), used_filename)
643
          )
644
        end
645
      end
646

647
      true
×
648
    end
649

650
    # Removes all xml and ycp files from directory where
651
    #
652
    # FIXME: this function seems to be unused, remove it?
653
    def CleanWorkflowsDirectory
1✔
654
      directory = GetWorkflowDirectory()
×
655
      Builtins.y2milestone(
×
656
        "Removing all xml and ycp files from '%1' directory",
657
        directory
658
      )
659

660
      if FileUtils.Exists(directory)
×
661
        # doesn't add RPM dependency on tar
662
        cmd = Convert.to_map(
×
663
          SCR.Execute(
664
            path(".target.bash_ouptut"),
665
            "\n" \
666
            "cd '%1';\n" \
667
            "/usr/bin/test -x /usr/bin/tar && /usr/bin/tar -zcf workflows_backup.tgz *.xml *.ycp *.rb;\n" \
668
            "/usr/bin/rm -rf *.xml *.ycp *.rb",
669
            String.Quote(directory)
670
          )
671
        )
672

673
        Builtins.y2error("Removing failed: %1", cmd) if Ops.get_integer(cmd, "exit", -1) != 0
×
674
      end
675

UNCOV
676
      nil
×
677
    end
678

679
    # Replace a module in a proposal with a set of other modules
680
    #
681
    # @param [Hash] proposal a map describing the proposal
682
    # @param [String] old string the old item to be replaced
683
    # @param [Array<String>] new a list of items to be put into instead of the old one
684
    # @return a map with the updated proposal
685
    def ReplaceProposalModule(proposal, old, new)
1✔
686
      proposal = deep_copy(proposal)
×
687
      new = deep_copy(new)
×
688
      found = false
×
689

690
      modules = Builtins.maplist(Ops.get_list(proposal, "proposal_modules", [])) do |m|
×
691
        if (Ops.is_string?(m) && Convert.to_string(m) == old) ||
×
692
            (Ops.is_map?(m) &&
×
693
                Ops.get_string(Convert.to_map(m), "name", "") == old)
694
          found = true
×
695

696
          next deep_copy(new) unless Ops.is_map?(m)
×
697

698
          Builtins.maplist(new) do |it|
×
699
            Builtins.union(Convert.to_map(m), "name" => it)
×
700
          end
701
        else
702
          [m]
×
703
        end
704
      end
705

706
      Builtins.y2internal("Replace/Remove proposal item %1 not found", old) if !found
×
707

708
      Ops.set(proposal, "proposal_modules", Builtins.flatten(modules))
×
709

710
      if Builtins.haskey(proposal, "proposal_tabs")
×
711
        Ops.set(
×
712
          proposal,
713
          "proposal_tabs",
714
          Builtins.maplist(Ops.get_list(proposal, "proposal_tabs", [])) do |tab|
715
            modules2 = Builtins.maplist(
×
716
              Ops.get_list(tab, "proposal_modules", [])
717
            ) do |m|
718
              (m == old) ? deep_copy(new) : [m]
×
719
            end
720

721
            Ops.set(tab, "proposal_modules", Builtins.flatten(modules2))
×
722
            deep_copy(tab)
×
723
          end
724
        )
725
      end
726

727
      deep_copy(proposal)
×
728
    end
729

730
    # Merge add-on proposal to a base proposal
731
    #
732
    # @param [Hash] base with the current product proposal
733
    # @param [Hash] additional_control with additional control file settings
734
    # @param [String] prod_name a name of the add-on product
735
    # @return [Hash] merged proposals
736
    def MergeProposal(base, additional_control, prod_name, domain)
1✔
737
      base = deep_copy(base)
1✔
738
      additional_control = deep_copy(additional_control)
1✔
739
      # Additional proposal settings - Replacing items
740
      replaces = Builtins.listmap(
1✔
741
        Ops.get_list(additional_control, "replace_modules", [])
742
      ) do |one_addon|
743
        old = Ops.get_string(one_addon, "replace", "")
×
744
        new = Ops.get_list(one_addon, "modules", [])
×
745
        { old => new }
×
746
      end
747

748
      if Ops.greater_than(
1✔
749
        Builtins.size(replaces),
750
        0
751
      )
752
        Builtins.foreach(replaces) do |old, new|
×
753
          base = ReplaceProposalModule(base, old, new)
×
754
        end
755
      end
756

757
      # Additional proposal settings - Removing settings
758
      removes = Ops.get_list(additional_control, "remove_modules", [])
1✔
759

760
      Builtins.foreach(removes) { |r| base = ReplaceProposalModule(base, r, []) } if Ops.greater_than(
1✔
761
        Builtins.size(removes),
762
        0
763
      )
764

765
      # Additional proposal settings - - Appending settings
766
      appends = Ops.get_list(additional_control, "append_modules", [])
1✔
767

768
      if Ops.greater_than(Builtins.size(appends), 0)
1✔
769
        append2 = deep_copy(appends)
×
770

771
        if Ops.is_map?(Ops.get(base, ["proposal_modules", 0]))
×
772
          append2 = Builtins.maplist(appends) do |m|
×
773
            { "name" => m, "presentation_order" => 9999 }
×
774
          end
775
        end
776

777
        Ops.set(
×
778
          base,
779
          "proposal_modules",
780
          Builtins.merge(Ops.get_list(base, "proposal_modules", []), append2)
781
        )
782

783
        if Builtins.haskey(base, "proposal_tabs")
×
784
          new_tab = {
785
            "label"            => prod_name,
×
786
            "proposal_modules" => appends,
787
            "textdomain"       => domain
788
          }
789
          Ops.set(
×
790
            base,
791
            "proposal_tabs",
792
            Builtins.add(Ops.get_list(base, "proposal_tabs", []), new_tab)
793
          )
794
        end
795
      end
796

797
      Ops.set(base, "enable_skip", "no") if Ops.get_string(additional_control, "enable_skip", "yes") == "no"
1✔
798

799
      deep_copy(base)
1✔
800
    end
801

802
    # Update system proposals according to proposal update metadata
803
    #
804
    # @param [Array<Hash>] proposals a list of update proposals
805
    # @param [String] prod_name string the product name (used in case of tabs)
806
    # @param [String] domain string the text domain (for translations)
807
    # @return [Boolean] true on success
808
    def UpdateProposals(proposals, prod_name, domain)
1✔
809
      proposals = deep_copy(proposals)
2✔
810
      Builtins.foreach(proposals) do |proposal|
2✔
811
        name = Ops.get_string(proposal, "name", "")
2✔
812
        stage = Ops.get_string(proposal, "stage", "")
2✔
813
        mode = Ops.get_string(proposal, "mode", "")
2✔
814
        arch = Ops.get_string(proposal, "archs", "")
2✔
815
        found = false
2✔
816
        new_proposals = []
2✔
817
        arch_all_prop = {}
2✔
818
        Builtins.foreach(ProductControl.proposals) do |p|
2✔
819
          if Ops.get_string(p, "stage", "") != stage ||
17✔
820
              Ops.get_string(p, "mode", "") != mode ||
821
              Ops.get_string(p, "name", "") != name
822
            new_proposals = Builtins.add(new_proposals, p)
16✔
823
            next
16✔
824
          end
825
          if [Ops.get_string(p, "archs", ""), "", "all"].include?(arch)
1✔
826
            p = MergeProposal(p, proposal, prod_name, domain)
1✔
827
            found = true
1✔
828
          elsif ["", "all"].include?(Ops.get_string(p, "archs", ""))
×
829
            arch_all_prop = deep_copy(p)
×
830
          end
831
          new_proposals = Builtins.add(new_proposals, p)
1✔
832
        end
833
        if !found
2✔
834
          if arch_all_prop == {}
1✔
835
            Ops.set(proposal, "textdomain", domain)
1✔
836
          else
837
            Ops.set(arch_all_prop, "archs", arch)
×
838
            proposal = MergeProposal(arch_all_prop, proposal, prod_name, domain)
×
839
            # completly new proposal
840
          end
841

842
          new_proposals = Builtins.add(new_proposals, proposal)
1✔
843
        end
844
        ProductControl.proposals = deep_copy(new_proposals)
2✔
845
      end
846

847
      true
2✔
848
    end
849

850
    # Replace a module in a workflow with a set of other modules
851
    #
852
    # @param [Hash] workflow a map describing the workflow
853
    # @param [String] old string the old item to be replaced
854
    # @param [Array<Hash>] new a list of items to be put into instead of the old one
855
    # @param [String] domain string a text domain
856
    # @param [Boolean] keep boolean true to keep original one (and just insert before)
857
    # @return a map with the updated workflow
858
    def ReplaceWorkflowModule(workflow, old, new, domain, keep)
1✔
859
      workflow = deep_copy(workflow)
3✔
860
      new = deep_copy(new)
3✔
861
      found = false
3✔
862

863
      modules = Builtins.maplist(Ops.get_list(workflow, "modules", [])) do |m|
3✔
864
        next [m] if Ops.get_string(m, "name", "") != old
6✔
865

866
        new_list = Builtins.maplist(new) do |n|
2✔
867
          Ops.set(n, "textdomain", domain)
2✔
868
          deep_copy(n)
2✔
869
        end
870

871
        found = true
2✔
872

873
        new_list = Builtins.add(new_list, m) if keep
2✔
874

875
        deep_copy(new_list)
2✔
876
      end
877

878
      log.warn("Insert/Replace/Remove workflow module '#{old}' not found") if !found
3✔
879
      Ops.set(workflow, "modules", Builtins.flatten(modules))
3✔
880
      deep_copy(workflow)
3✔
881
    end
882

883
    # Merge add-on workflow to a base workflow
884
    #
885
    # @param [Hash] base map the base product workflow
886
    # @param [Hash] addon map the workflow of the addon product
887
    # @param [String] prod_name a name of the add-on product
888
    # @return [Hash] merged workflows
889
    def MergeWorkflow(base, addon, _prod_name, domain)
1✔
890
      base = deep_copy(base)
1✔
891
      addon = deep_copy(addon)
1✔
892

893
      log.info "merging workflow #{addon.inspect} to #{base.inspect}"
1✔
894

895
      # Merging - removing steps, settings
896
      removes = Ops.get_list(addon, "remove_modules", [])
1✔
897

898
      if Ops.greater_than(Builtins.size(removes), 0)
1✔
899
        Builtins.y2milestone("Remove: %1", removes)
×
900
        Builtins.foreach(removes) do |r|
×
901
          base = ReplaceWorkflowModule(base, r, [], domain, false)
×
902
        end
903
      end
904

905
      # Merging - replacing steps, settings
906
      replaces = Builtins.listmap(Ops.get_list(addon, "replace_modules", [])) do |a|
1✔
907
        old = Ops.get_string(a, "replace", "")
×
908
        new = Ops.get_list(a, "modules", [])
×
909
        { old => new }
×
910
      end
911

912
      if Ops.greater_than(Builtins.size(replaces), 0)
1✔
913
        Builtins.y2milestone("Replace: %1", replaces)
×
914
        Builtins.foreach(replaces) do |old, new|
×
915
          base = ReplaceWorkflowModule(base, old, new, domain, false)
×
916
        end
917
      end
918

919
      # Merging - inserting steps, settings
920
      inserts = Builtins.listmap(Ops.get_list(addon, "insert_modules", [])) do |i|
1✔
921
        before = Ops.get_string(i, "before", "")
×
922
        new = Ops.get_list(i, "modules", [])
×
923
        { before => new }
×
924
      end
925

926
      if Ops.greater_than(Builtins.size(inserts), 0)
1✔
927
        Builtins.y2milestone("Insert: %1", inserts)
×
928
        Builtins.foreach(inserts) do |old, new|
×
929
          base = ReplaceWorkflowModule(base, old, new, domain, true)
×
930
        end
931
      end
932

933
      # Merging - appending steps, settings
934
      appends = Ops.get_list(addon, "append_modules", [])
1✔
935

936
      if Ops.greater_than(Builtins.size(appends), 0)
1✔
937
        Builtins.y2milestone("Append: %1", appends)
1✔
938
        Builtins.foreach(appends) do |new|
1✔
939
          Ops.set(new, "textdomain", domain)
4✔
940
          Ops.set(
4✔
941
            base,
942
            "modules",
943
            Builtins.add(Ops.get_list(base, "modules", []), new)
944
          )
945
        end
946
      end
947

948
      log.info "result of merge #{base.inspect}"
1✔
949
      deep_copy(base)
1✔
950
    end
951

952
    # Update system workflows according to workflow update metadata
953
    #
954
    # @param [Array<Hash>] workflows a list of update workflows
955
    # @param [String] prod_name string the product name (used in case of tabs)
956
    # @param [String] domain string the text domain (for translations)
957
    # @return [Boolean] true on success
958
    def UpdateWorkflows(workflows, prod_name, domain)
1✔
959
      workflows = deep_copy(workflows)
3✔
960
      Builtins.foreach(workflows) do |workflow|
3✔
961
        stage = Ops.get_string(workflow, "stage", "")
3✔
962
        mode = Ops.get_string(workflow, "mode", "")
3✔
963
        arch = Ops.get_string(workflow, "archs", "")
3✔
964
        found = false
3✔
965
        new_workflows = []
3✔
966
        arch_all_wf = {}
3✔
967
        log.info "workflow to update #{workflow.inspect}"
3✔
968

969
        Builtins.foreach(ProductControl.workflows) do |w|
3✔
970
          if Ops.get_string(w, "stage", "") != stage ||
13✔
971
              Ops.get_string(w, "mode", "") != mode
972
            new_workflows = Builtins.add(new_workflows, w)
12✔
973
            next
12✔
974
          end
975
          if [Ops.get_string(w, ["defaults", "archs"], ""), "", "all"].include?(arch)
1✔
976
            w = MergeWorkflow(w, workflow, prod_name, domain)
1✔
977
            found = true
1✔
978
          elsif ["", "all"].include?(Ops.get_string(w, ["defaults", "archs"], ""))
×
979
            arch_all_wf = deep_copy(w)
×
980
          end
981
          new_workflows = Builtins.add(new_workflows, w)
1✔
982
        end
983
        if !found
3✔
984
          if arch_all_wf == {}
2✔
985
            # If modules has not been defined we are trying to use the appended modules
986
            workflow["modules"] = workflow["append_modules"] unless workflow["modules"]
2✔
987

988
            Ops.set(workflow, "textdomain", domain)
2✔
989

990
            Ops.set(
2✔
991
              workflow,
992
              "modules",
993
              Builtins.maplist(Ops.get_list(workflow, "modules", [])) do |mod|
994
                Ops.set(mod, "textdomain", domain)
8✔
995
                deep_copy(mod)
8✔
996
              end
997
            )
998
          else
999
            Ops.set(arch_all_wf, ["defaults", "archs"], arch)
×
1000
            workflow = MergeWorkflow(arch_all_wf, workflow, prod_name, domain)
×
1001
            # completly new workflow
1002
          end
1003

1004
          new_workflows = Builtins.add(new_workflows, workflow)
2✔
1005
        end
1006

1007
        log.info "new workflow after update #{new_workflows}"
3✔
1008

1009
        ProductControl.workflows = deep_copy(new_workflows)
3✔
1010
      end
1011

1012
      true
3✔
1013
    end
1014

1015
    # Update sytem roles according to the update section of the control file
1016
    #
1017
    # The hash is expectd to have the following structure:
1018
    #
1019
    # "insert_system_roles" => [
1020
    #   {
1021
    #    "system_roles" =>
1022
    #      [
1023
    #        { "id" => "additional_role1" },
1024
    #        { "id" => "additional_role2" }
1025
    #      ]
1026
    #   }
1027
    # ]
1028
    #
1029
    # @param new_roles [Hash] System roles specification
1030
    #
1031
    # @see ProductControl#add_system_roles
1032
    def update_system_roles(system_roles)
1✔
1033
      system_roles.fetch("insert_system_roles", []).each do |insert|
3✔
1034
        ProductControl.add_system_roles(insert["system_roles"])
1✔
1035
      end
1036
    end
1037

1038
    # Add specified steps to inst_finish.
1039
    # Just modifies internal variables, inst_finish grabs them itself
1040
    #
1041
    # @param [Hash{String => Array<String>}] additional_steps a map specifying the steps to be added
1042
    # @return [Boolean] true on success
1043
    def UpdateInstFinish(additional_steps)
1✔
1044
      additional_steps = deep_copy(additional_steps)
×
1045
      before_chroot = Ops.get(additional_steps, "before_chroot", [])
×
1046
      after_chroot = Ops.get(additional_steps, "after_chroot", [])
×
1047
      before_umount = Ops.get(additional_steps, "before_umount", [])
×
1048

1049
      @additional_finish_steps_before_chroot = Convert.convert(
×
1050
        Builtins.merge(@additional_finish_steps_before_chroot, before_chroot),
1051
        from: "list",
1052
        to:   "list <string>"
1053
      )
1054

1055
      @additional_finish_steps_after_chroot = Convert.convert(
×
1056
        Builtins.merge(@additional_finish_steps_after_chroot, after_chroot),
1057
        from: "list",
1058
        to:   "list <string>"
1059
      )
1060

1061
      @additional_finish_steps_before_umount = Convert.convert(
×
1062
        Builtins.merge(@additional_finish_steps_before_umount, before_umount),
1063
        from: "list",
1064
        to:   "list <string>"
1065
      )
1066

1067
      true
×
1068
    end
1069

1070
    # Adapts the current workflow according to specified XML file content
1071
    #
1072
    # @param [Hash] update_file a map containing the additional product control file
1073
    # @param [String] name string the name of the additional product
1074
    # @param [String] domain string the text domain for the additional control file
1075
    #
1076
    # @return [Boolean] true on success
1077
    def UpdateInstallation(update_file, name, domain)
1✔
1078
      log.info "Updating installation workflow: #{update_file.inspect}"
3✔
1079
      update_file = deep_copy(update_file)
3✔
1080
      PrepareSystemProposals()
3✔
1081
      PrepareSystemWorkflows()
3✔
1082

1083
      proposals = Ops.get_list(update_file, "proposals", [])
3✔
1084
      proposals = PrepareProposals(proposals)
3✔
1085
      UpdateProposals(proposals, name, domain)
3✔
1086

1087
      workflows = Ops.get_list(update_file, "workflows", [])
3✔
1088
      workflows = PrepareWorkflows(workflows)
3✔
1089
      UpdateWorkflows(workflows, name, domain)
3✔
1090

1091
      update_system_roles(update_file.fetch("system_roles", {}))
3✔
1092

1093
      true
3✔
1094
    end
1095

1096
    # Add new defined proposal to the list of system proposals
1097
    #
1098
    # @param [Array<Hash>] proposals a list of proposals to be added
1099
    # @return [Boolean] true on success
1100
    def AddNewProposals(proposals)
1✔
1101
      proposals = deep_copy(proposals)
×
1102
      forbidden = Builtins.maplist(ProductControl.proposals) do |p|
×
1103
        Ops.get_string(p, "name", "")
×
1104
      end
1105

1106
      forbidden = Builtins.toset(forbidden)
×
1107

1108
      Builtins.foreach(proposals) do |proposal|
×
1109
        if Builtins.contains(forbidden, Ops.get_string(proposal, "name", ""))
×
1110
          Builtins.y2warning(
×
1111
            "Proposal '%1' already exists, not adding",
1112
            Ops.get_string(proposal, "name", "")
1113
          )
1114
        else
1115
          Builtins.y2milestone(
×
1116
            "Adding new proposal %1",
1117
            Ops.get_string(proposal, "name", "")
1118
          )
1119
          ProductControl.proposals = Builtins.add(
×
1120
            ProductControl.proposals,
1121
            proposal
1122
          )
1123
        end
1124
      end
1125

1126
      true
×
1127
    end
1128

1129
    # Replace workflows for 2nd stage of installation
1130
    #
1131
    # @param [Array<Hash>] workflows a list of the workflows
1132
    # @return [Boolean] true on success
1133
    def Replaceworkflows(workflows)
1✔
1134
      workflows = deep_copy(workflows)
×
1135
      workflows = PrepareWorkflows(workflows)
×
1136

1137
      # This function doesn't update the current workflow but replaces it.
1138
      # That's why it is not allowed for the first stage of the installation.
1139
      workflows = Builtins.filter(workflows) do |workflow|
×
1140
        if Ops.get_string(workflow, "stage", "") == "initial"
×
1141
          Builtins.y2error(
×
1142
            "Attempting to replace 1st stage workflow. This is not possible"
1143
          )
1144
          Builtins.y2milestone("Workflow: %1", workflow)
×
1145
          next false
×
1146
        end
1147
        true
×
1148
      end
1149

1150
      sm = {}
×
1151

1152
      Builtins.foreach(workflows) do |workflow|
×
1153
        Ops.set(
×
1154
          sm,
1155
          Ops.get_string(workflow, "stage", ""),
1156
          Ops.get(sm, Ops.get_string(workflow, "stage", ""), {})
1157
        )
1158
        Ops.set(
×
1159
          sm,
1160
          [
1161
            Ops.get_string(workflow, "stage", ""),
1162
            Ops.get_string(workflow, "mode", "")
1163
          ],
1164
          true
1165
        )
1166
        [
1167
          Ops.get_string(workflow, "stage", ""),
×
1168
          Ops.get_string(workflow, "mode", "")
1169
        ]
1170
      end
1171

1172
      Builtins.y2milestone("Existing replace workflows: %1", sm)
×
1173
      Builtins.y2milestone(
×
1174
        "Workflows before filtering: %1",
1175
        Builtins.size(ProductControl.workflows)
1176
      )
1177

1178
      ProductControl.workflows = Builtins.filter(ProductControl.workflows) do |w|
×
1179
        !Ops.get(
×
1180
          sm,
1181
          [Ops.get_string(w, "stage", ""), Ops.get_string(w, "mode", "")],
1182
          false
1183
        )
1184
      end
1185

1186
      Builtins.y2milestone(
×
1187
        "Workflows after filtering: %1",
1188
        Builtins.size(ProductControl.workflows)
1189
      )
1190
      ProductControl.workflows = Convert.convert(
×
1191
        Builtins.merge(ProductControl.workflows, workflows),
1192
        from: "list",
1193
        to:   "list <map>"
1194
      )
1195

1196
      true
×
1197
    end
1198

1199
    # Returns list of workflows requiring registration
1200
    #
1201
    # @see FATE #305578: Add-On Product Requiring Registration
1202
    def WorkflowsRequiringRegistration
1✔
1203
      deep_copy(@workflows_requiring_registration)
×
1204
    end
1205

1206
    # Returns whether a repository workflow requires registration
1207
    #
1208
    # @param [Fixnum] src_id
1209
    # @return [Boolean] if registration is required
1210
    def WorkflowRequiresRegistration(src_id)
1✔
1211
      ret = false
×
1212

1213
      Builtins.y2milestone("Known workflows: %1", @workflows_to_sources)
×
1214
      Builtins.y2milestone(
×
1215
        "Workflows requiring registration: %1",
1216
        @workflows_requiring_registration
1217
      )
1218

1219
      Builtins.foreach(@workflows_to_sources) do |one_workflow, id|
×
1220
        # sources match and workflow is listed as 'requiring registration'
1221
        if src_id == id &&
×
1222
            Builtins.contains(@workflows_requiring_registration, one_workflow)
1223
          ret = true
×
1224
          raise Break
×
1225
        end
1226
      end
1227

1228
      Builtins.y2milestone("WorkflowRequiresRegistration(%1): %2", src_id, ret)
×
1229
      ret
×
1230
    end
1231

1232
    # Read and remember the registration requirement status from an installation
1233
    # control XML file.
1234
    # The stored values can be read by the WorkflowsRequiringRegistration() method.
1235
    # @param filename [String] path to the XML file
1236
    # @return [Boolean] true if the file has been read properly, false in case of an error
1237
    def IncorporateControlFileOptions(filename)
1✔
1238
      return false if filename.nil?
×
1239

1240
      begin
1241
        update_file = XML.XMLToYCPFile(filename)
×
1242
      rescue RuntimeError => e
1243
        log.error "Unable to read the #{filename} control file: #{e.inspect}"
×
1244
        return false
×
1245
      end
1246

1247
      # FATE #305578: Add-On Product Requiring Registration
1248
      globals = Ops.get_map(update_file, "globals", {})
×
1249

1250
      if Builtins.haskey(globals, "require_registration") &&
×
1251
          Ops.get_boolean(globals, "require_registration", false) == true
1252
        Builtins.y2milestone("Registration is required by %1", filename)
×
1253
        @workflows_requiring_registration = Builtins.toset(
×
1254
          Builtins.add(@workflows_requiring_registration, filename)
1255
        )
1256
        Builtins.y2milestone(
×
1257
          "Workflows requiring registration: %1",
1258
          @workflows_requiring_registration
1259
        )
1260
      else
1261
        Builtins.y2milestone("Registration is not required by %1", filename)
×
1262
      end
1263

1264
      true
×
1265
    end
1266

1267
    # Update product options such as global settings, software, partitioning
1268
    # or network.
1269
    #
1270
    # @param [Hash] update_file a map containing update control file
1271
    # @param
1272
    # @return [Boolean] true on success
1273
    def UpdateProductInfo(update_file, _filename)
1✔
1274
      update_file = deep_copy(update_file)
×
1275
      # merging all 'map <string, any>' type
1276
      Builtins.foreach(["globals", "software", "partitioning", "network"]) do |section|
×
1277
        sect = ProductFeatures.GetSection(section)
×
1278
        addon = Ops.get_map(update_file, section, {})
×
1279
        sect = Convert.convert(
×
1280
          Builtins.union(sect, addon),
1281
          from: "map",
1282
          to:   "map <string, any>"
1283
        )
1284
        ProductFeatures.SetSection(section, sect)
×
1285
      end
1286

1287
      # merging 'clone_modules'
1288
      addon_clone = Ops.get_list(update_file, "clone_modules", [])
×
1289
      ProductControl.clone_modules = Convert.convert(
×
1290
        Builtins.merge(ProductControl.clone_modules, addon_clone),
1291
        from: "list",
1292
        to:   "list <string>"
1293
      )
1294

1295
      # merging texts
1296

1297
      #
1298
      # **Structure:**
1299
      #
1300
      #     $[
1301
      #        "congratulate" : $[
1302
      #          "label" : "some text",
1303
      #        ],
1304
      #        "congratulate2" : $[
1305
      #          "label" : "some other text",
1306
      #          "textdomain" : "control-2", // (optionally)
1307
      #        ],
1308
      #      ];
1309
      controlfile_texts = ProductFeatures.GetSection("texts")
×
1310
      update_file_texts = Ops.get_map(update_file, "texts", {})
×
1311
      update_file_textdomain = Ops.get_string(update_file, "textdomain", "")
×
1312

1313
      # if textdomain is different to the base one
1314
      # we have to put it into the map
1315
      if !update_file_textdomain.nil? && update_file_textdomain != ""
×
1316
        update_file_texts = Builtins.mapmap(update_file_texts) do |text_ident, text_def|
×
1317
          Ops.set(text_def, "textdomain", update_file_textdomain)
×
1318
          { text_ident => text_def }
×
1319
        end
1320
      end
1321

1322
      controlfile_texts = Convert.convert(
×
1323
        Builtins.union(controlfile_texts, update_file_texts),
1324
        from: "map",
1325
        to:   "map <string, any>"
1326
      )
1327
      ProductFeatures.SetSection("texts", controlfile_texts)
×
1328

1329
      true
×
1330
    end
1331

1332
    # Redraws workflow steps. Function must be called when steps (or help for steps)
1333
    # are active. It doesn't work in case of active another dialog.
1334
    def RedrawWizardSteps
1✔
1335
      Builtins.y2milestone("Retranslating messages, redrawing wizard steps")
×
1336

1337
      # Make sure the labels for default function keys are retranslated, too.
1338
      # Using Label::DefaultFunctionKeyMap() from Label module.
1339
      UI.SetFunctionKeys(Label.DefaultFunctionKeyMap)
×
1340

1341
      # Activate language changes on static part of wizard dialog
1342
      ProductControl.RetranslateWizardSteps
×
1343
      Wizard.RetranslateButtons
×
1344
      Wizard.SetFocusToNextButton
×
1345

1346
      true
×
1347
    end
1348

1349
    # Integrate the changes in the workflow
1350
    # @param [String] filename string filename of the control file (local filename)
1351
    # @return [Boolean] true on success
1352
    def IntegrateWorkflow(filename)
1✔
1353
      Builtins.y2milestone("IntegrateWorkflow %1", filename)
10✔
1354

1355
      begin
1356
        update_file = XML.XMLToYCPFile(filename)
10✔
1357
      rescue RuntimeError => e
1358
        log.error "Failed to parse #{update_file}: #{e.inspect}"
1✔
1359
        return false
1✔
1360
      end
1361

1362
      name = Ops.get_string(update_file, "display_name", "")
9✔
1363

1364
      if !UpdateInstallation(
9✔
1365
        Ops.get_map(update_file, "update", {}),
1366
        name,
1367
        Ops.get_string(update_file, "textdomain", "control")
1368
      )
1369
        Builtins.y2error("Failed to update installation workflow")
1✔
1370
        return false
1✔
1371
      end
1372

1373
      if !UpdateProductInfo(update_file, filename)
8✔
1374
        Builtins.y2error("Failed to set product options")
1✔
1375
        return false
1✔
1376
      end
1377

1378
      if !AddNewProposals(Ops.get_list(update_file, "proposals", []))
7✔
1379
        Builtins.y2error("Failed to add new proposals")
1✔
1380
        return false
1✔
1381
      end
1382

1383
      if !Replaceworkflows(Ops.get_list(update_file, "workflows", []))
6✔
1384
        Builtins.y2error("Failed to replace workflows")
1✔
1385
        return false
1✔
1386
      end
1387

1388
      if !UpdateInstFinish(
5✔
1389
        Ops.get_map(update_file, ["update", "inst_finish"], {})
1390
      )
1391
        Builtins.y2error("Adding inst_finish steps failed")
1✔
1392
        return false
1✔
1393
      end
1394

1395
      true
4✔
1396
    end
1397

1398
    # Returns file unique identification in format <file_MD5sum>-<file_size>
1399
    # Returns 'nil' if file doesn't exist, it is not a 'file', etc.
1400
    #
1401
    # @param string file
1402
    # @return [String] file_ident
1403
    def GenerateWorkflowIdent(workflow_filename)
1✔
1404
      file_md5sum = FileUtils.MD5sum(workflow_filename)
×
1405

1406
      if file_md5sum.nil? || file_md5sum == ""
×
1407
        Builtins.y2error(
×
1408
          "MD5 sum of file %1 is %2",
1409
          workflow_filename,
1410
          file_md5sum
1411
        )
1412
        return nil
×
1413
      end
1414

1415
      file_size = FileUtils.GetSize(workflow_filename)
×
1416

1417
      if Ops.less_than(file_size, 0)
×
1418
        Builtins.y2error("File size %1 is %2", workflow_filename, file_size)
×
1419
        return nil
×
1420
      end
1421

1422
      Builtins.sformat("%1-%2", file_md5sum, file_size)
×
1423
    end
1424

1425
    # Function uses the Base Workflow as the initial one and merges all
1426
    # added workflow into that workflow.
1427
    #
1428
    # @return [Boolean] if successful
1429
    def MergeWorkflows
1✔
1430
      Builtins.y2milestone("Merging additional control files from scratch...")
×
1431
      @unmerged_changes = false
×
1432

1433
      # Init the Base Workflow settings
1434
      FillUpInitialWorkflowSettings()
×
1435

1436
      ret = true
×
1437

1438
      already_merged_workflows = []
×
1439

1440
      @merge_counter += 1
×
1441
      add_on_counter = 1
×
1442

1443
      Builtins.foreach(@used_workflows) do |one_workflow|
×
1444
        # make sure that every workflow is merged only once
1445
        # bugzilla #332436
1446
        workflow_ident = GenerateWorkflowIdent(one_workflow)
×
1447
        if !workflow_ident.nil? &&
×
1448
            Builtins.contains(already_merged_workflows, workflow_ident)
1449
          Builtins.y2milestone(
×
1450
            "The very same workflow has been already merged, skipping..."
1451
          )
1452
          next
×
1453
        elsif !workflow_ident.nil?
×
1454
          already_merged_workflows = Builtins.add(
×
1455
            already_merged_workflows,
1456
            workflow_ident
1457
          )
1458
        else
1459
          Builtins.y2error("Workflow ident is: %1", workflow_ident)
×
1460
        end
1461

1462
        # log the installation.xml being merged
1463

1464
        control_log_dir_rotator = Yast2::ControlLogDirRotator.new
×
1465
        control_log_dir_rotator.copy(one_workflow, "/#{format("%02d", @merge_counter)}-#{format("%02d", add_on_counter)}-installation.xml")
×
1466
        add_on_counter += 1
×
1467

1468
        IncorporateControlFileOptions(one_workflow)
×
1469
        if !IntegrateWorkflow(one_workflow)
×
1470
          Builtins.y2error("Merging '%1' failed!", one_workflow)
×
1471
          Report.Error(
×
1472
            _(
1473
              "An internal error occurred when integrating additional workflow."
1474
            )
1475
          )
1476
          ret = false
×
1477
        end
1478
      end
1479

1480
      ret
×
1481
    end
1482

1483
    # Returns whether some additional control files were added or removed
1484
    # from the last time MergeWorkflows() was called.
1485
    #
1486
    # @return boolen see description
1487
    def SomeWorkflowsWereChanged
1✔
1488
      @unmerged_changes
×
1489
    end
1490

1491
    # Returns list of control-file names currently used
1492
    #
1493
    # @return [Array<String>] files
1494
    def GetAllUsedControlFiles
1✔
1495
      deep_copy(@used_workflows)
×
1496
    end
1497

1498
    # Sets list of control-file names to be used.
1499
    # ATTENTION: this is dangerous and should be used in rare cases only!
1500
    #
1501
    # @see #GetAllUsedControlFiles()
1502
    # @param list <string> new workflows (XML files in absolute-path format)
1503
    # @example
1504
    #  SetAllUsedControlFiles (["/tmp/new_addon_control.xml", "/root/special_addon.xml"]);
1505
    def SetAllUsedControlFiles(new_list)
1✔
1506
      new_list = deep_copy(new_list)
×
1507
      Builtins.y2milestone("New list of additional workflows: %1", new_list)
×
1508
      @unmerged_changes = true
×
1509
      @used_workflows = deep_copy(new_list)
×
1510

UNCOV
1511
      nil
×
1512
    end
1513

1514
    # Returns whether some additional control files are currently in use.
1515
    #
1516
    # @return [Boolean] some additional control files are in use.
1517
    def HaveAdditionalWorkflows
1✔
1518
      Ops.greater_or_equal(Builtins.size(GetAllUsedControlFiles()), 0)
×
1519
    end
1520

1521
    # Returns the current settings used by WorkflowManager.
1522
    # This function is just for debugging purpose.
1523
    #
1524
    # @return [Hash{String => Object}] of current settings
1525
    #
1526
    # **Structure:**
1527
    #
1528
    #     [
1529
    #         "workflows" : ...
1530
    #         "proposals" : ...
1531
    #         "inst_finish" : ...
1532
    #         "clone_modules" : ...
1533
    #         "system_roles" : ...
1534
    #         "unmerged_changes" : ...
1535
    #       ];
1536
    def DumpCurrentSettings
1✔
1537
      {
1538
        "workflows"        => ProductControl.workflows,
5✔
1539
        "proposals"        => ProductControl.proposals,
1540
        "inst_finish"      => ProductControl.inst_finish,
1541
        "clone_modules"    => ProductControl.clone_modules,
1542
        "system_roles"     => ProductControl.system_roles,
1543
        "unmerged_changes" => @unmerged_changes
1544
      }
1545
    end
1546

1547
    # Merge product's workflow
1548
    #
1549
    # @param product [Y2Packager::Product] Base product
1550
    def merge_product_workflow(product)
1✔
1551
      return false unless product.installation_package
3✔
1552

1553
      log.info "Merging #{product.label} workflow"
3✔
1554

1555
      if merged_base_product
3✔
1556
        Yast::WorkflowManager.RemoveWorkflow(
1✔
1557
          :package,
1558
          merged_base_product.installation_package_repo,
1559
          merged_base_product.installation_package
1560
        )
1561
      end
1562

1563
      AddWorkflow(:package, product.installation_package_repo, product.installation_package)
3✔
1564
      MergeWorkflows()
3✔
1565
      RedrawWizardSteps()
3✔
1566
      self.merged_base_product = product
3✔
1567
    end
1568

1569
    # Merge modules extensions
1570
    #
1571
    # @param packages [Array<String>] packages that extends workflow
1572
    def merge_modules_extensions(packages)
1✔
1573
      log.info "Merging #{packages} workflow"
3✔
1574

1575
      merged_modules_extensions.each do |pkg|
3✔
1576
        Yast::WorkflowManager.RemoveWorkflow(:package, 0, pkg)
2✔
1577
      end
1578

1579
      packages.each do |pkg|
3✔
1580
        AddWorkflow(:package, 0, pkg)
6✔
1581
      end
1582
      MergeWorkflows()
3✔
1583
      RedrawWizardSteps()
3✔
1584

1585
      self.merged_modules_extensions = packages
3✔
1586
    end
1587

1588
    publish variable: :additional_finish_steps_before_chroot, type: "list <string>"
1✔
1589
    publish variable: :additional_finish_steps_after_chroot, type: "list <string>"
1✔
1590
    publish variable: :additional_finish_steps_before_umount, type: "list <string>"
1✔
1591
    publish function: :GetAdditionalFinishSteps, type: "list <string> (string)"
1✔
1592
    publish function: :SetBaseWorkflow, type: "void (boolean)"
1✔
1593
    publish function: :PrepareProposals, type: "list <map> (list <map>)"
1✔
1594
    publish function: :PrepareSystemProposals, type: "void ()"
1✔
1595
    publish function: :PrepareWorkflows, type: "list <map> (list <map>)"
1✔
1596
    publish function: :PrepareSystemWorkflows, type: "void ()"
1✔
1597
    publish function: :ResetWorkflow, type: "void ()"
1✔
1598
    publish function: :GetCachedWorkflowFilename, type: "string (symbol, integer, string)"
1✔
1599
    publish function: :AddWorkflow, type: "boolean (symbol, integer, string)"
1✔
1600
    publish function: :RemoveWorkflow, type: "boolean (symbol, integer, string)"
1✔
1601
    publish function: :CleanWorkflowsDirectory, type: "void ()"
1✔
1602
    publish function: :WorkflowsRequiringRegistration, type: "list <string> ()"
1✔
1603
    publish function: :WorkflowRequiresRegistration, type: "boolean (integer)"
1✔
1604
    publish function: :IncorporateControlFileOptions, type: "boolean (string)"
1✔
1605
    publish function: :RedrawWizardSteps, type: "boolean ()"
1✔
1606
    publish function: :MergeWorkflows, type: "boolean ()"
1✔
1607
    publish function: :SomeWorkflowsWereChanged, type: "boolean ()"
1✔
1608
    publish function: :GetAllUsedControlFiles, type: "list <string> ()"
1✔
1609
    publish function: :SetAllUsedControlFiles, type: "void (list <string>)"
1✔
1610
    publish function: :HaveAdditionalWorkflows, type: "boolean ()"
1✔
1611
    publish function: :DumpCurrentSettings, type: "map <string, any> ()"
1✔
1612

1613
  private
1✔
1614

1615
    # @return [Y2Packager::Product,nil] Product or nil if no base product workflow was merged.
1616
    attr_accessor :merged_base_product
1✔
1617

1618
    # @return [Array<String>] list of modules that have registered extensions
1619
    attr_accessor :merged_modules_extensions
1✔
1620

1621
    # Find the product from a repository.
1622
    # @param repo_id [Fixnum] repository ID
1623
    # @return [Hash,nil] pkg-bindings product hash or nil if not found
1624
    def find_product(repo_id)
1✔
1625
      # identify the product
1626
      products = Y2Packager::Resolvable.find(kind: :product)
21✔
1627
      return nil unless products
21✔
1628

1629
      products.select! { |p| p.source == repo_id }
41✔
1630

1631
      if products.size > 1
21✔
1632
        log.warn("More than one product found in the repository: #{products}")
×
1633
        log.warn("Using the first one: #{products.first.name}")
×
1634
      end
1635

1636
      products.first
21✔
1637
    end
1638

1639
    # Find the extension package name for the specified release package.
1640
    # The extension package is defined by the "installerextension()"
1641
    # RPM "Provides" dependency.
1642
    # @param  [Y2Packager::Resolvable] release package
1643
    # @return [String,nil] a package name or nil if not found
1644
    def find_control_package(release_package)
1✔
1645
      return nil unless release_package&.deps
19✔
1646

1647
      release_package.deps.each do |dep|
18✔
1648
        provide = dep["provides"]
17✔
1649
        next unless provide
17✔
1650

1651
        control_file_package = provide[/\Ainstallerextension\((.+)\)\z/, 1]
17✔
1652
        next unless control_file_package
17✔
1653

1654
        log.info("Found referenced package with control file: #{control_file_package}")
16✔
1655
        return control_file_package.strip
16✔
1656
      end
1657

1658
      nil
2✔
1659
    end
1660

1661
    # Find the repository ID for the package.
1662
    # @param package_name [String] name of the package
1663
    # @return [Fixnum,nil] repository ID or nil if not found
1664
    def package_repository(package_name)
1✔
1665
      # Identify the installation repository with the package
1666
      pkgs = Y2Packager::Resolvable.find(kind: :package, name: package_name)
16✔
1667

1668
      if pkgs.empty?
16✔
1669
        log.warn("The installer extension package #{package_name} was not found")
1✔
1670
        return nil
1✔
1671
      end
1672

1673
      latest_package = pkgs.reduce(nil) do |a, p|
15✔
1674
        (!a || (Pkg.CompareVersions(a.version, p.version) < 0)) ? p : a
15✔
1675
      end
1676

1677
      if pkgs.size > 1
15✔
1678
        log.warn("More than one control package found: #{pkgs}")
×
1679
        log.info("Using the latest package: #{latest_package}")
×
1680
      end
1681

1682
      latest_package.source
15✔
1683
    end
1684

1685
    # Download and extract a package from a repository.
1686
    # @param repo_id [Fixnum] repository ID
1687
    # @param package [String] name of the package
1688
    # @raise [::Packages::PackageDownloader::FetchError] if package download failed
1689
    # @raise [Y2Packager::PackageExtractionError] if package extraction failed
1690
    def fetch_package(repo_id, package, dir)
1✔
1691
      downloader = ::Packages::PackageDownloader.new(repo_id, package)
15✔
1692

1693
      Tempfile.open("downloaded-package-") do |tmp|
15✔
1694
        # libzypp needs the target for verifying the GPG signatures of the downloaded packages,
1695
        # keep the target initialized, it might be needed later for verifying other packages
1696
        # However, avoid this call when running on update mode because we need the repositories
1697
        # from the system to upgrade too.
1698
        Pkg.TargetInitialize("/") if Stage.initial && !Mode.update
15✔
1699
        downloader.download(tmp.path)
15✔
1700

1701
        extract(tmp.path, dir)
13✔
1702
        # the RPM package file is not needed after extracting it's content,
1703
        # remove it explicitly now, do not wait for the garbage collector
1704
        # (in inst-syst it is stored in a RAM disk and eats the RAM memory)
1705
        tmp.unlink
11✔
1706
      end
1707
    end
1708

1709
    # Extract an RPM package into the given directory.
1710
    # @param package_file [String] the RPM package path
1711
    # @param dir [String] a directory where the package will be extracted to
1712
    # @raise [::Y2Packager::PackageExtractionError] if package extraction failed
1713
    def extract(package_file, dir)
1✔
1714
      log.info("Extracting file #{package_file}")
13✔
1715
      extractor = ::Packages::PackageExtractor.new(package_file)
13✔
1716
      extractor.extract(dir)
13✔
1717
    end
1718
  end
1719

1720
  WorkflowManager = WorkflowManagerClass.new
1✔
1721
  WorkflowManager.main
1✔
1722
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