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

yast / yast-ntp-client / 7348969670

28 Dec 2023 03:24PM UTC coverage: 64.902% (+0.5%) from 64.369%
7348969670

Pull #176

github

web-flow
Merge 77c9312b9 into fdb352f7a
Pull Request #176: Update rubocop

29 of 39 new or added lines in 12 files covered. (74.36%)

4 existing lines in 3 files now uncovered.

956 of 1473 relevant lines covered (64.9%)

20.25 hits per line

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

83.5
/src/modules/NtpClient.rb
1
# Package:  Configuration of ntp-client
2
# Summary:  Data for configuration of ntp-client, input and output functions.
3
# Authors:  Jiri Srain <jsrain@suse.cz>
4
#
5
# $Id$
6
#
7
# Representation of the configuration of ntp-client.
8
# Input and output routines.
9
require "yast"
1✔
10
require "yaml"
1✔
11
require "cfa/chrony_conf"
1✔
12
require "yast2/target_file" # required to cfa work on changed scr
1✔
13
require "ui/text_helpers"
1✔
14
require "erb"
1✔
15
require "yast2/systemctl"
1✔
16
require "y2network/ntp_server"
1✔
17

18
module Yast
1✔
19
  class NtpClientClass < Module
1✔
20
    include Logger
1✔
21
    include ::UI::TextHelpers
1✔
22

23
    # the default synchronization interval in minutes when running in the manual
24
    # sync mode ("Synchronize without Daemon" option, ntp started from systemd timer)
25
    # Note: the UI field currently uses maximum of 60 minutes
26
    DEFAULT_SYNC_INTERVAL = 5
1✔
27

28
    # the default netconfig policy for ntp
29
    DEFAULT_NTP_POLICY = "auto".freeze
1✔
30

31
    # List of servers defined by the pool.ntp.org to get random ntp servers
32
    #
33
    # @see #http://www.pool.ntp.org/
34
    RANDOM_POOL_NTP_SERVERS = ["0.pool.ntp.org", "1.pool.ntp.org", "2.pool.ntp.org"].freeze
1✔
35

36
    NTP_FILE = "/etc/chrony.d/pool.conf".freeze
1✔
37

38
    TIMER_FILE = "yast-timesync.timer".freeze
1✔
39
    # The file name of systemd timer for the synchronization.
40
    TIMER_PATH = "/etc/systemd/system/#{TIMER_FILE}".freeze
1✔
41

42
    # FIXME: We should avoid the use of the full path as it could be problematic with or without
43
    # usr-merge (bsc#1205401)
44
    # @return [String] Netconfig executable
45
    NETCONFIG_PATH = "/sbin/netconfig".freeze
1✔
46

47
    UNSUPPORTED_AUTOYAST_OPTIONS = [
48
      "configure_dhcp",
1✔
49
      "peers",
50
      "restricts",
51
      "start_at_boot",
52
      "start_in_chroot",
53
      "sync_interval",
54
      "synchronize_time"
55
    ].freeze
56

57
    # Package which is needed for saving NTP configuration into system
58
    REQUIRED_PACKAGE = "chrony".freeze
1✔
59

60
    def main
1✔
61
      textdomain "ntp-client"
72✔
62

63
      Yast.import "Directory"
72✔
64
      Yast.import "FileUtils"
72✔
65
      Yast.import "Lan"
72✔
66
      Yast.import "Message"
72✔
67
      Yast.import "Mode"
72✔
68
      Yast.import "Package"
72✔
69
      Yast.import "Popup"
72✔
70
      Yast.import "Progress"
72✔
71
      Yast.import "ProductFeatures"
72✔
72
      Yast.import "Report"
72✔
73
      Yast.import "Service"
72✔
74
      Yast.import "SLPAPI"
72✔
75
      Yast.import "Stage"
72✔
76
      Yast.import "String"
72✔
77
      Yast.import "Summary"
72✔
78
      Yast.import "UI"
72✔
79

80
      # Abort function
81
      # return boolean return true if abort
82
      @AbortFunction = nil
72✔
83

84
      # Data was modified?
85
      @modified = false
72✔
86

87
      # Write only, used during autoinstallation.
88
      # Don't run services and SuSEconfig, it's all done at one place.
89
      @write_only = false
72✔
90

91
      # Should the daemon be started when system boots?
92
      @run_service = true
72✔
93

94
      # Should the time synchronized periodicaly?
95
      @synchronize_time = false
72✔
96

97
      # The interval of synchronization in minutes.
98
      @sync_interval = DEFAULT_SYNC_INTERVAL
72✔
99

100
      # Service names of the NTP daemon
101
      @service_name = "chronyd"
72✔
102

103
      # "chrony-wait" service has also to be handled in order to ensure that
104
      # "chronyd" is working correctly and do not depend on the network status.
105
      # bsc#1137196, bsc#1129730
106
      @wait_service_name = "chrony-wait"
72✔
107

108
      # Netconfig policy: for merging and prioritizing static and DHCP config.
109
      # https://github.com/openSUSE/sysconfig/blob/master/doc/README.netconfig
110
      # https://github.com/openSUSE/sysconfig/blob/master/config/sysconfig.config-network
111
      @ntp_policy = DEFAULT_NTP_POLICY
72✔
112

113
      # Active Directory controller
114
      @ad_controller = ""
72✔
115

116
      # Required packages
117
      @required_packages = [REQUIRED_PACKAGE]
72✔
118

119
      # List of known NTP servers
120
      # server address -> information
121
      #  address: the key repeated
122
      #  country: CC (uppercase)
123
      #  location: for displaying
124
      #  ...: (others are unused)
125
      @ntp_servers = nil
72✔
126

127
      # Mapping between country codes and country names ("CZ" -> "Czech Republic")
128
      @country_names = nil
72✔
129

130
      @config_has_been_read = false
72✔
131

132
      # for lazy loading
133
      @countries_already_read = false
72✔
134
      @known_countries = {}
72✔
135

136
      @random_pool_servers = RANDOM_POOL_NTP_SERVERS
72✔
137

138
      # helper variable to hold config from ntp client proposal
139
      @ntp_selected = false
72✔
140
    end
141

142
    # CFA instance for reading/writing /etc/chrony.conf
143
    def ntp_conf
1✔
144
      @ntp_conf ||= CFA::ChronyConf.new
127✔
145
    end
146

147
    # Abort function
148
    # @return blah blah lahjk
149
    def Abort
1✔
150
      @AbortFunction.nil? ? false : @AbortFunction.call == true
×
151
    end
152

153
    def go_next
1✔
154
      return false if Abort()
1✔
155

156
      Progress.NextStage if progress?
×
157
      true
×
158
    end
159

160
    def progress?
1✔
161
      Mode.normal
×
162
    end
163

164
    # return [Boolean] whether netconfig is present in the system or not
165
    def netconfig?
1✔
166
      FileUtils.Exists(NETCONFIG_PATH)
×
167
    end
168

169
    # Synchronize against specified server only one time and does not modify
170
    # any configuration
171
    # @param server [String] to sync against
172
    # @return [Integer] exit code of sync command
173
    def sync_once(server)
1✔
174
      log.info "Running one time sync with #{server}"
2✔
175

176
      # -q: set system time and quit
177
      # -t: timeout in seconds
178
      # -l <file>: log to a file to not mess text mode installation
179
      # -c: causes all IP addresses to which ntp_server resolves to be queried in parallel
180
      ret = SCR.Execute(
2✔
181
        path(".target.bash_output"),
182
        # TODO: ensure that we can use always pool instead of server?
183
        "/usr/sbin/chronyd -q -t 30 'pool #{String.Quote(server)} iburst'"
184
      )
185
      log.info "'one-time chrony for #{server}' returned #{ret}"
2✔
186

187
      ret["exit"]
2✔
188
    end
189

190
    # Given a country code and a location returns a hash with pool
191
    # ntp address for given country, country code and location
192
    # @return [Hash{String => String}] ntp pool address for given country
193
    def MakePoolRecord(country_code, location)
1✔
194
      mycc = country_code.downcase
1,409✔
195
      # There is no gb.pool.ntp.org only uk.pool.ntp.org
196
      mycc = "uk" if mycc == "gb"
1,409✔
197
      {
198
        "address"  => "#{mycc}.pool.ntp.org",
1,409✔
199
        "country"  => country_code,
200
        "location" => location
201
      }
202
    end
203

204
    # Returns the know ntp servers
205
    #
206
    # @return [Array<Y2Network::NtpServer>] Known NTP servers
207
    def public_ntp_servers
1✔
208
      update_ntp_servers! if @ntp_servers.nil?
10✔
209
      @ntp_servers.values.map do |srv|
10✔
210
        Y2Network::NtpServer.new(
2,328✔
211
          srv["address"], country: srv["country"], location: srv["location"]
212
        )
213
      end
214
    end
215

216
    # Returns the NTP servers for the given country
217
    #
218
    # @param country [String] Country code
219
    # @return [Array<Y2Network::NtpServer>] NTP servers for the given country
220
    def country_ntp_servers(country)
1✔
221
      normalized_country = country.upcase
3✔
222
      servers = public_ntp_servers.select { |s| s.country.upcase == normalized_country }
587✔
223
      # bnc#458917 add country, in case data/country.ycp does not have it
224
      country_server = make_country_ntp_server(country)
3✔
225
      servers << country_server unless servers.map(&:hostname).include?(country_server.hostname)
3✔
226
      servers
3✔
227
    end
228

229
    # Get the list of known NTP servers
230
    # @deprecated Use public_ntp_servers instead
231
    # @return a list of known NTP servers
232
    def GetNtpServers
1✔
233
      update_ntp_servers! if @ntp_servers.nil?
15✔
234

235
      deep_copy(@ntp_servers)
15✔
236
    end
237

238
    # Get the mapping between country codes and names ("CZ" -> "Czech Republic")
239
    # @return a map the country codes and names mapping
240
    def GetCountryNames
1✔
241
      if @country_names.nil?
12✔
242
        @country_names = Convert.convert(
12✔
243
          Builtins.eval(SCR.Read(path(".target.yast2"), "country.ycp")),
244
          from: "any",
245
          to:   "map <string, string>"
246
        )
247
      end
248
      if @country_names.nil?
12✔
249
        Builtins.y2error("Failed to read country names")
×
250
        @country_names = {}
×
251
      end
252
      deep_copy(@country_names)
12✔
253
    end
254

255
    # Get list of public NTP servers for a country
256
    #
257
    # @param [String] country two-letter country code
258
    # @param [Boolean] terse_output display additional data (location etc.)
259
    # @return [Array] of servers (usable as combo-box items)
260
    # @deprecated Use public_ntp_servers_by_country instead
261
    def GetNtpServersByCountry(country, terse_output)
1✔
262
      country_names = {}
2✔
263
      servers = GetNtpServers()
2✔
264
      if country.to_s == ""
2✔
265
        country_names = GetCountryNames()
2✔
266
      else
UNCOV
267
        servers.select! { |_server, attrs| attrs["country"] == country }
×
268
        # bnc#458917 add country, in case data/country.ycp does not have it
269
        pool_country_record = MakePoolRecord(country, "")
×
270
        servers[pool_country_record["address"]] = pool_country_record
×
271
      end
272

273
      default = false
2✔
274
      servers.map do |server, attrs|
2✔
275
        # Select the first occurrence of pool.ntp.org as the default option (bnc#940881)
276
        selected = default ? false : default = server.end_with?("pool.ntp.org")
580✔
277

278
        next Item(Id(server), server, selected) if terse_output
580✔
279

280
        country_label = country.empty? ? country_names[attrs["country"]] || attrs["country"] : ""
580✔
281

282
        label = server + country_server_label(attrs["location"].to_s, country_label.to_s)
580✔
283

284
        Item(Id(server), label, selected)
580✔
285
      end
286
    end
287

288
    def read_ntp_conf
1✔
289
      if !FileUtils.Exists(NTP_FILE)
6✔
290
        log.error("File #{NTP_FILE} does not exist")
2✔
291
        return false
2✔
292
      end
293

294
      begin
295
        ntp_conf.load
4✔
296
      rescue StandardError => e
297
        log.error("Failed to read #{NTP_FILE}: #{e.message}")
×
298
        return false
×
299
      end
300

301
      true
4✔
302
    end
303

304
    # Read and parse /etc/ntp.conf
305
    # @return true on success
306
    def ProcessNtpConf
1✔
307
      if @config_has_been_read
9✔
308
        log.info "Configuration has been read already, skipping."
3✔
309
        return false
3✔
310
      end
311

312
      return false unless read_ntp_conf
6✔
313

314
      @config_has_been_read = true
4✔
315

316
      true
4✔
317
    end
318

319
    # Read the synchronization status, fill
320
    # synchronize_time and sync_interval variables
321
    # Return updated value of synchronize_time
322
    def ReadSynchronization
1✔
323
      return false unless ::File.exist?(TIMER_PATH)
8✔
324

325
      timer_content = ::File.read(TIMER_PATH)
7✔
326
      log.info("NTP Synchronization timer entry: #{timer_content}")
7✔
327
      @synchronize_time = Yast2::Systemctl.execute("is-active #{TIMER_FILE}").exit.zero?
7✔
328

329
      interval = timer_content[/^\s*OnUnitActiveSec\s*=\s*(\d+)m/, 1]
7✔
330
      @sync_interval = interval.to_i if interval
7✔
331
      log.info("SYNC_INTERVAL #{@sync_interval}")
7✔
332

333
      @synchronize_time
7✔
334
    end
335

336
    # Read all ntp-client settings
337
    # @return true on success
338
    def Read
1✔
339
      log.info("NtpClient::Read - enter")
28✔
340

341
      return true if @config_has_been_read
28✔
342

343
      # We do not set help text here, because it was set outside
344
      new_read_progress if progress?
11✔
345

346
      # read network configuration
347
      return false if !go_next
11✔
348

349
      progress_orig = Progress.set(false)
11✔
350
      Progress.set(progress_orig)
11✔
351

352
      read_policy!
11✔
353
      GetNtpServers()
11✔
354
      GetCountryNames()
11✔
355

356
      # read current settings
357
      return false if !go_next
11✔
358

359
      if !Mode.installation && !Package.CheckAndInstallPackagesInteractive(["chrony"])
11✔
360
        log.info("Package::CheckAndInstallPackagesInteractive failed")
1✔
361
        return false
1✔
362
      end
363

364
      @run_service = Service.Enabled(@service_name)
10✔
365

366
      # Poke to /var/lib/YaST if there is Active Directory controller address dumped in .ycp file
367
      read_ad_address!
10✔
368

369
      ProcessNtpConf()
10✔
370
      ReadSynchronization()
10✔
371

372
      return false if !go_next
10✔
373

374
      Progress.Title(_("Finished")) if progress?
10✔
375

376
      return false if Abort()
10✔
377

378
      @modified = false
9✔
379
      true
9✔
380
    end
381

382
    # Function returns list of NTP servers used in the configuration.
383
    #
384
    # @return [Array<String>] of servers
385
    def GetUsedNtpServers
1✔
386
      ntp_conf.pools.keys
3✔
387
    end
388

389
    # @return [Hash<String, Symbol> pair of source address and type (server, pool)
390
    def GetUsedNtpSources
1✔
391
      ntp_conf.servers.keys.each_with_object(sources = {}) { |s, res| res[s] = :server }
47✔
392
      ntp_conf.pools.keys.each_with_object(sources) { |s, res| res[s] = :pool }
47✔
393
    end
394

395
    # Write all ntp-client settings
396
    # @return true on success
397
    def Write
1✔
398
      # We do not set help text here, because it was set outside
399
      new_write_progress if progress?
10✔
400

401
      # write settings
402
      return false if !go_next
10✔
403

404
      Report.Error(Message.CannotWriteSettingsTo("/etc/chrony.conf")) if !write_ntp_conf
9✔
405

406
      if netconfig?
9✔
407
        write_and_update_policy
8✔
408
      else
409
        log.info("There is no netconfig, skipping policy write")
1✔
410
      end
411

412
      # restart daemon
413
      return false if !go_next
9✔
414

415
      check_service
9✔
416

417
      update_timer_settings
9✔
418

419
      return false if !go_next
9✔
420

421
      Progress.Title(_("Finished")) if progress?
9✔
422

423
      !Abort()
9✔
424
    end
425

426
    # Get all ntp-client settings from the first parameter
427
    # (For use by autoinstallation.)
428
    # @param [Hash] settings The YCP structure to be imported.
429
    # @return [Boolean] True on success
430
    def Import(settings)
1✔
431
      log.info "Import with #{settings}"
11✔
432

433
      unsupported = UNSUPPORTED_AUTOYAST_OPTIONS.select { |o| settings.key?(o) }
88✔
434
      if !unsupported.empty?
11✔
435
        unsupported_error(unsupported)
×
436
        return false
×
437
      end
438

439
      sync = (settings["ntp_sync"] || "systemd").strip
11✔
440
      case sync
11✔
441
      when "systemd"
442
        @run_service = true
5✔
443
        @synchronize_time = false
5✔
444
      when /[0-9]/
445
        @run_service = false
4✔
446
        @synchronize_time = true
4✔
447
        @sync_interval = sync.to_i
4✔
448
        # if wrong number is passed log it and use default
449
        if !(1..59).cover?(@sync_interval)
4✔
450
          log.error "Invalid interval in sync interval #{@sync_interval}"
×
451
          @sync_interval = DEFAULT_SYNC_INTERVAL
×
452
        end
453
      when /manual/
454
        @run_service = false
2✔
455
        @synchronize_time = false
2✔
456
      else
457
        # TRANSLATORS: error report. %s stands for invalid content.
458
        Yast::Report.Error(format(_("Invalid value for ntp_sync key: '%s'"), sync))
×
459
        return false
×
460
      end
461

462
      @modified = true
11✔
463
      @ntp_policy = settings["ntp_policy"] || DEFAULT_NTP_POLICY
11✔
464
      ntp_conf.clear_pools
11✔
465
      (settings["ntp_servers"] || []).each do |server|
11✔
466
        options = {}
4✔
467
        options["iburst"] = nil if server["iburst"]
4✔
468
        options["offline"] = nil if server["offline"]
4✔
469
        address = server["address"]
4✔
470
        log.info "adding server '#{address.inspect}' with options #{options.inspect}"
4✔
471
        ntp_conf.add_pool(address, options)
4✔
472
      end
473

474
      true
11✔
475
    end
476

477
    # Merges config to existing system configuration. It is useful for delayed write.
478
    # When it at first set values, then chrony is installed and then it writes. So
479
    # before write it will merge to system. Result is that it keep majority of config
480
    # untouched and modify what is needed.
481
    # What it mean is that if it set values, it works on parsed configuration file,
482
    # but if package is not yet installed, then it creates new configuration file
483
    # which is missing many stuff like comments or values that yast2-ntp-client does not touch.
484
    # So if package is installed later, then this method re-apply changes on top of newly parsed
485
    # file.
486
    def merge_to_system
1✔
487
      config = Export()
2✔
488
      Read()
2✔
489
      Import(config)
2✔
490
    end
491

492
    # Summary text about ntp configuration
493
    def Summary
1✔
494
      result = ""
1✔
495
      sync_line = if @run_service
1✔
496
        _("The NTP daemon starts when starting the system.")
×
497
      elsif @synchronize_time
1✔
498
        # TRANSLATORS %i is number of seconds.
499
        format(_("The NTP will be synchronized every %i seconds."), @sync_interval)
×
500
      else
501
        _("The NTP won't be automatically synchronized.")
1✔
502
      end
503
      result = Yast::Summary.AddLine(result, sync_line)
1✔
504
      policy_line = case @ntp_policy
1✔
505
      when "auto"
506
        _("Combine static and DHCP configuration.")
1✔
507
      when ""
508
        _("Static configuration only.")
×
509
      else
510
        format(_("Custom configuration policy: '%s'."), @ntp_policy)
×
511
      end
512
      result = Yast::Summary.AddLine(result, policy_line)
1✔
513
      # TRANSLATORS: summary line. %s is formatted list of addresses.
514
      servers_line = format(_("Servers: %s."), GetUsedNtpServers().join(", "))
1✔
515
      Yast::Summary.AddLine(result, servers_line)
1✔
516
    end
517

518
    # Dump the ntp-client settings to a single map
519
    # (For use by autoinstallation.)
520
    # @return [Hash] Dumped settings (later acceptable by Import ())
521
    def Export
1✔
522
      sync_value = if @run_service
5✔
523
        "systemd"
1✔
524
      elsif @synchronize_time
4✔
525
        @sync_interval.to_s
1✔
526
      else
527
        "manual"
3✔
528
      end
529
      pools_export = ntp_conf.pools.map do |(address, options)|
5✔
530
        {
531
          "address" => address,
2✔
532
          "iburst"  => options.key?("iburst"),
533
          "offline" => options.key?("offline")
534
        }
535
      end
536
      {
537
        "ntp_sync"    => sync_value,
5✔
538
        "ntp_policy"  => @ntp_policy,
539
        "ntp_servers" => pools_export
540
      }
541
    end
542

543
    # Test if a specified NTP server is reachable by IPv4 or IPv6 (bsc#74076),
544
    # Firewall could have been blocked IPv6
545
    # @param [String] server string host name or IP address of the NTP server
546
    # @return [Boolean] true if NTP server answers properly
547
    def reachable_ntp_server?(server)
1✔
548
      ntp_test(server) || ntp_test(server, 6)
5✔
549
    end
550

551
    # Test NTP server answer for a given IP version.
552
    # @param [String] server string host name or IP address of the NTP server
553
    # @param [Integer] ip_version ip version to use (4 or 6)
554
    # @return [Boolean] true if stderr does not include lookup error and exit
555
    # code is 0
556
    def ntp_test(server, ip_version = 4)
1✔
557
      output = SCR.Execute(
7✔
558
        path(".target.bash_output"),
559
        # -t : seconds of timeout
560
        # -Q: print only offset, if failed exit is non-zero
561
        "LANG=C /usr/sbin/chronyd -#{ip_version} -t 30 -Q 'pool #{server} iburst'"
562
      )
563

564
      Builtins.y2milestone("chronyd test response: #{output}")
7✔
565

566
      output["exit"] == 0
7✔
567
    end
568

569
    # Handle UI of NTP server test answers
570
    # @param [String] server string host name or IP address of the NTP server
571
    # @param [Symbol] verbosity `no_ui: ..., `transient_popup: pop up while scanning,
572
    #                  `result_popup: also final pop up about the result
573
    # @return [Boolean] true if NTP server answers properly
574
    def TestNtpServer(server, verbosity)
1✔
575
      return reachable_ntp_server?(server) if verbosity == :no_ui
8✔
576

577
      ok = false
7✔
578
      Yast::Popup.Feedback(_("Testing the NTP server..."), Message.takes_a_while) do
7✔
579
        log.info("Testing reachability of server #{server}")
5✔
580
        ok = reachable_ntp_server?(server)
5✔
581
      end
582

583
      if verbosity == :result_popup
7✔
584
        if ok
3✔
585
          # message report - result of test of connection to NTP server
586
          Popup.Notify(_("Server is reachable and responds properly."))
1✔
587
        else
588
          # error message  - result of test of connection to NTP server
589
          # report error instead of simple message (#306018)
590
          Report.Error(_("Server is unreachable or does not respond properly."))
2✔
591
        end
592
      end
593
      ok
7✔
594
    end
595

596
    # Detect NTP servers present in the local network
597
    # @param [Symbol] method symbol method of the detection (only `slp suported ATM)
598
    # @return a list of found NTP servers
599
    def DetectNtpServers(method)
1✔
600
      if method == :slp
×
601
        required_package = "yast2-slp"
×
602

603
        # if package is not installed (in the inst-sys, it is: bnc#399659)
604
        if !Stage.initial && !Package.Installed(required_package)
×
NEW
605
          if Package.CheckAndInstallPackages([required_package])
×
NEW
606
            SCR.RegisterAgent(path(".slp"), term(:ag_slp, term(:SlpAgent)))
×
607
          else
UNCOV
608
            Report.Error(
×
609
              Builtins.sformat(
610
                _(
611
                  "Cannot search for NTP server in local network\nwithout package %1 installed.\n"
612
                ),
613
                required_package
614
              )
615
            )
616
            Builtins.y2warning("Not searching for local NTP servers via SLP")
×
617
            return []
×
618
          end
619
        end
620

621
        servers = SLPAPI.FindSrvs("service:ntp", "")
×
622
        server_names = Builtins.maplist(servers) do |m|
×
623
          Ops.get_string(m, "pcHost", "")
×
624
        end
625
        server_names = Builtins.filter(server_names) { |s| s != "" }
×
626
        return deep_copy(server_names)
×
627
      end
628
      Builtins.y2error("Unknown detection method: %1", method)
×
629
      []
×
630
    end
631

632
    # Return required packages for auto-installation
633
    # @return [Hash] of packages to be installed and to be removed
634
    def AutoPackages
1✔
635
      { "install" => @required_packages, "remove" => [] }
2✔
636
    end
637

638
    # Convenience method to obtain the list of ntp servers proposed by DHCP
639
    # @see https://www.rubydoc.info/github/yast/yast-network/Yast/LanClass:${0}
640
    def dhcp_ntp_servers
1✔
641
      Yast::Lan.dhcp_ntp_servers.map { |s| Y2Network::NtpServer.new(s) }
8✔
642
    end
643

644
    publish variable: :AbortFunction, type: "boolean ()"
1✔
645
    publish variable: :modified, type: "boolean"
1✔
646
    publish variable: :write_only, type: "boolean"
1✔
647
    publish variable: :run_service, type: "boolean"
1✔
648
    publish variable: :synchronize_time, type: "boolean"
1✔
649
    publish variable: :sync_interval, type: "integer"
1✔
650
    publish variable: :service_name, type: "string"
1✔
651
    publish variable: :ntp_policy, type: "string"
1✔
652
    publish variable: :ntp_selected, type: "boolean"
1✔
653
    publish variable: :ad_controller, type: "string"
1✔
654
    publish variable: :config_has_been_read, type: "boolean"
1✔
655
    publish function: :GetNtpServers, type: "map <string, map <string, string>> ()"
1✔
656
    publish function: :GetCountryNames, type: "map <string, string> ()"
1✔
657
    publish function: :GetNtpServersByCountry, type: "list (string, boolean)"
1✔
658
    publish function: :ProcessNtpConf, type: "boolean ()"
1✔
659
    publish function: :ReadSynchronization, type: "boolean ()"
1✔
660
    publish function: :Read, type: "boolean ()"
1✔
661
    publish function: :GetUsedNtpServers, type: "list <string> ()"
1✔
662
    publish variable: :random_pool_servers, type: "list <string>"
1✔
663
    publish function: :Write, type: "boolean ()"
1✔
664
    publish function: :Import, type: "boolean (map)"
1✔
665
    publish function: :Export, type: "map ()"
1✔
666
    publish function: :Summary, type: "string ()"
1✔
667
    publish function: :TestNtpServer, type: "boolean (string, symbol)"
1✔
668
    publish function: :DetectNtpServers, type: "list <string> (symbol)"
1✔
669
    publish function: :AutoPackages, type: "map ()"
1✔
670

671
  private
1✔
672

673
    # Reads and returns all known countries with their country codes
674
    #
675
    # @return [Hash{String => String}] of known contries
676
    #
677
    # **Structure:**
678
    #
679
    #     $[
680
    #        "CL" : "Chile",
681
    #        "FR" : "France",
682
    #        ...
683
    #      ]
684
    def GetAllKnownCountries
1✔
685
      # first point of dependence on yast2-country-data
686
      if !@countries_already_read
16✔
687
        @known_countries = Convert.convert(
16✔
688
          Builtins.eval(
689
            SCR.Read(
690
              path(".target.ycp"),
691
              Directory.find_data_file("country.ycp")
692
            )
693
          ),
694
          from: "any",
695
          to:   "map <string, string>"
696
        )
697
        @countries_already_read = true
16✔
698
        @known_countries = {} if @known_countries.nil?
16✔
699
      end
700

701
      # workaround bug #241054: servers in United Kingdom are in domain .uk
702
      # domain .gb does not exist - add UK to the list of known countries
703
      if Builtins.haskey(@known_countries, "GB")
16✔
704
        Ops.set(@known_countries, "UK", Ops.get(@known_countries, "GB", ""))
16✔
705
        @known_countries = Builtins.remove(@known_countries, "GB")
16✔
706
      end
707

708
      deep_copy(@known_countries)
16✔
709
    end
710

711
    # Set @ntp_policy according to NETCONFIG_NTP_POLICY value found in
712
    # /etc/sysconfig/network/config or with {DEFAULT_NTP_POLICY} if not found
713
    #
714
    # @return [String] read value or {DEFAULT_NTP_POLICY} as default
715
    def read_policy!
1✔
716
      # SCR::Read may return nil (no such value in sysconfig, file not there etc. )
717
      # set if not nil, otherwise use 'auto' as safe fallback (#449362)
718
      @ntp_policy = SCR.Read(path(".sysconfig.network.config.NETCONFIG_NTP_POLICY")) ||
1✔
719
        DEFAULT_NTP_POLICY
720
    end
721

722
    # Set @ad_controller according to ad_ntp_data["ads"] value found in
723
    # data_file ad_ntp_data.ycp if exists.
724
    #
725
    # Removes the file if some value is read.
726
    def read_ad_address!
1✔
727
      ad_ntp_file = Directory.find_data_file("ad_ntp_data.ycp")
3✔
728
      if ad_ntp_file
3✔
729
        log.info("Reading #{ad_ntp_file}")
2✔
730
        ad_ntp_data = SCR.Read(path(".target.ycp"), ad_ntp_file)
2✔
731

732
        @ad_controller = ad_ntp_data["ads"].to_s if ad_ntp_data
2✔
733
        if @ad_controller != ""
2✔
734
          Builtins.y2milestone(
2✔
735
            "Got %1 for ntp sync, deleting %2, since it is no longer needed",
736
            @ad_controller,
737
            ad_ntp_file
738
          )
739
          SCR.Execute(path(".target.remove"), ad_ntp_file)
2✔
740
        end
741
      else
742
        log.info "There is no active directory data's file available."
1✔
743
      end
744
    end
745

746
    # Set @ntp_servers with known servers and known countries pool ntp servers
747
    def update_ntp_servers!
1✔
748
      @ntp_servers = {}
18✔
749

750
      read_known_servers.each { |s| cache_server(s) }
2,724✔
751

752
      pool_servers_for(GetAllKnownCountries()).each { |p| cache_server(p) }
1,422✔
753
    end
754

755
    # Start a new progress for Read NTP Configuration
756
    def new_read_progress
1✔
757
      Progress.New(
×
758
        _("Initializing NTP Client Configuration"),
759
        " ",
760
        2,
761
        [
762
          # progress stage
763
          _("Read network configuration"),
764
          # progress stage
765
          _("Read NTP settings")
766
        ],
767
        [
768
          # progress step
769
          _("Reading network configuration..."),
770
          # progress step
771
          _("Reading NTP settings..."),
772
          # progress step
773
          _("Finished")
774
        ],
775
        ""
776
      )
777
    end
778

779
    # Start a new progress for Write NTP Configuration
780
    def new_write_progress
1✔
781
      Progress.New(
×
782
        _("Saving NTP Client Configuration"),
783
        " ",
784
        2,
785
        [
786
          # progress stage
787
          _("Write NTP settings"),
788
          # progress stage
789
          _("Restart NTP daemon")
790
        ],
791
        [
792
          # progress step
793
          _("Writing the settings..."),
794
          # progress step
795
          _("Restarting NTP daemon..."),
796
          # progress step
797
          _("Finished")
798
        ],
799
        ""
800
      )
801
    end
802

803
    def update_cfa_record(record)
1✔
804
      cfa_record = record["cfa_record"]
×
805
      cfa_record.value = record["address"]
×
806
      cfa_record.raw_options = record["options"]
×
807
      cfa_record.comment = record["comment"]
×
808
    end
809

810
    # Write current /etc/chrony.conf
811
    # @return [Boolean] true on success
812
    def write_ntp_conf
1✔
813
      begin
814
        ntp_conf.save
1✔
815
      rescue StandardError => e
816
        log.error("Failed to write #{NTP_FILE}: #{e.message}")
×
817
        return false
×
818
      end
819

820
      true
1✔
821
    end
822

823
    # Writes /etc/sysconfig/network/config NETCONFIG_NTP_POLICY
824
    # with current @ntp_policy value
825
    # @return [Boolean] true on success
826
    def write_policy
1✔
827
      SCR.Write(
×
828
        path(".sysconfig.network.config.NETCONFIG_NTP_POLICY"),
829
        @ntp_policy
830
      )
831
      SCR.Write(path(".sysconfig.network.config"), nil)
×
832
    end
833

834
    # Calls netconfig to update ntp
835
    # @return [Boolean] true on success
836
    def update_netconfig
1✔
837
      SCR.Execute(path(".target.bash"), "#{NETCONFIG_PATH} update -m ntp") == 0
×
838
    end
839

840
    # Writes sysconfig ntp policy and calls netconfig to update ntp. Report an
841
    # error if some of the call fails.
842
    #
843
    # @return [Boolean] true if write and update success
844
    def write_and_update_policy
1✔
845
      success = write_policy && update_netconfig
×
846

847
      Report.Error(_("Cannot update the dynamic configuration policy.")) unless success
×
848

849
      success
×
850
    end
851

852
    # Enable or disable chrony services depending on @run_service value
853
    # "chrony-wait" service has also to be handled in order to ensure that
854
    # "chronyd" is working correctly and do not depend on the network status.
855
    #
856
    # * When disabling, it also stops the services.
857
    # * When enabling, it tries to restart the services unless it's in write
858
    #   only mode.
859
    def check_service
1✔
860
      # fallbacks to false if not defined
861
      wait_service_required = ProductFeatures.GetBooleanFeature("globals", "precise_time")
3✔
862
      if @run_service
3✔
863
        # Enable and run services
864
        if !Service.Enable(@service_name)
2✔
865
          Report.Error(Message.CannotAdjustService(@service_name))
×
866
        elsif wait_service_required && !Service.Enable(@wait_service_name)
2✔
867
          Report.Error(Message.CannotAdjustService(@wait_service_name))
×
868
        end
869
        if !@write_only
2✔
870
          if !Service.Restart(@service_name)
2✔
871
            Report.Error(_("Cannot restart \"%s\" service.") % @service_name)
×
872
          elsif wait_service_required && !Service.Restart(@wait_service_name)
2✔
873
            Report.Error(_("Cannot restart \"%s\" service.") % @wait_service_name)
×
874
          end
875
        end
876
      else
877
        # Disable and stop services
878
        if !Service.Disable(@service_name)
1✔
879
          Report.Error(Message.CannotAdjustService(@service_name))
×
880
        # disable and stop always as wait without chrony does not make sense
881
        elsif !Service.Disable(@wait_service_name)
1✔
882
          Report.Error(Message.CannotAdjustService(@wait_service_name))
×
883
        end
884
        Service.Stop(@service_name)
1✔
885
        Service.Stop(@wait_service_name)
1✔
886
      end
887
    end
888

889
    def timer_content
1✔
890
      erb_template = ::File.read(Directory.find_data_file("#{TIMER_FILE}.erb"))
4✔
891
      content = ERB.new(erb_template)
4✔
892
      # warning on unused timeout is false positive - used in the erb loaded above
893
      timeout = @sync_interval
4✔
894
      content.result(binding)
4✔
895
    end
896

897
    # If synchronize time has been enable it writes systemd timer entry for manual
898
    # sync. If not it removes current systemd timer entry if exists.
899
    def update_timer_settings
1✔
900
      if @synchronize_time
8✔
901
        SCR.Write(
×
902
          path(".target.string"),
903
          TIMER_PATH,
904
          timer_content
905
        )
906
        res = Yast2::Systemctl.execute("enable #{TIMER_FILE}")
×
907
        log.info "enable timer: #{res.inspect}"
×
908
        res = Yast2::Systemctl.execute("start #{TIMER_FILE}")
×
909
        log.info "start timer: #{res.inspect}"
×
910
      else
911
        res = Yast2::Systemctl.execute("disable #{TIMER_FILE}")
8✔
912
        log.info "disable timer: #{res.inspect}"
8✔
913
        res = Yast2::Systemctl.execute("stop #{TIMER_FILE}")
8✔
914
        log.info "stop timer: #{res.inspect}"
8✔
915
        SCR.Execute(
8✔
916
          path(".target.bash"),
917
          "rm -vf #{TIMER_PATH}"
918
        )
919
      end
920
    end
921

922
    # Reads from file ntp servers list and return them. Return an empty hash if
923
    # not able to read the servers.
924
    #
925
    # @return [Hash] of ntp servers.
926
    def read_known_servers
1✔
927
      servers_file = Directory.find_data_file("ntp_servers.yml")
16✔
928

929
      return {} if !servers_file
16✔
930

931
      servers = YAML.load_file(servers_file)
16✔
932
      if servers.nil?
16✔
933
        log.error("Failed to read the list of NTP servers")
×
934
        return {}
×
935
      end
936

937
      log.info "Known NTP servers read: #{servers}"
16✔
938

939
      servers
16✔
940
    end
941

942
    # Returns a concatenation of given location and country depending on if
943
    # them are empty or not.
944
    #
945
    # @example
946
    #   country_server_label("Canary Islands", "Spain") # => " (Canary Islands, Spain)"
947
    #   country_server_label("Nürnberg", "")            # => " (Nürnberg)"
948
    #   country_server_label("", "Deutschland")         # => " (Deutschland)"
949
    #
950
    # @param [String] location of server
951
    # @param [String] country of server
952
    # @return [String] concatenate location and country if not empty
953
    def country_server_label(location = "", country = "")
1✔
954
      return "" if location.empty? && country.empty?
580✔
955
      return " (#{location}, #{country})" if !location.empty? && !country.empty?
580✔
956

957
      " (#{location}#{country})"
64✔
958
    end
959

960
    # Given a Hash of known countries, it returns a list of pool records for
961
    # each country.
962
    # @see #MakePoolRecord
963
    #
964
    # @param [Hash <String, String>] known_countries
965
    # @return [Array <Hash>] pool records for given countries
966
    def pool_servers_for(known_countries)
1✔
967
      known_countries.map do |short_country, country_name|
16✔
968
        # bnc#458917 add country, in case data/country.ycp does not have it
969
        MakePoolRecord(short_country, country_name)
1,404✔
970
      end
971
    end
972

973
    # Add given server to @ntp_server Hash using the server address as the key
974
    # and the server as the value
975
    #
976
    # @param [Hash <String, String>] server string host name or IP address of the NTP server
977
    # @return [Boolean] result of the assignation
978
    def cache_server(server)
1✔
979
      @ntp_servers[server["address"].to_s] = server
3,817✔
980
    end
981

982
    def unsupported_error(unsupported)
1✔
983
      msg = format(
×
984
        # TRANSLATORS: error report. %s stands unsupported keys.
985
        _("Ignoring the NTP configuration. The profile format has changed in an " \
986
          "incompatible way. These keys are no longer supported: '%s'."),
987
        unsupported.join("', '")
988
      )
989

990
      displayinfo = Yast::UI.GetDisplayInfo
×
991
      width = displayinfo["TextMode"] ? displayinfo.fetch("Width", 80) : 80
×
992

993
      Yast::Report.Error(wrap_text(msg, width - 4))
×
994
    end
995

996
    # Pool server for the given country
997
    #
998
    # @param country [String] Country code
999
    # @return [Y2Network::NtpServer]
1000
    def make_country_ntp_server(country)
1✔
1001
      record = MakePoolRecord(country, "")
3✔
1002
      Y2Network::NtpServer.new(record["address"], country: record["country"])
3✔
1003
    end
1004
  end
1005

1006
  NtpClient = NtpClientClass.new
1✔
1007
  NtpClient.main
1✔
1008
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