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

yast / yast-ntp-client / 3592083838

pending completion
3592083838

Pull #173

github

Unknown Committer
Unknown Commit Message
Pull Request #173: WIP: Updated ntp sources handling

166 of 166 new or added lines in 5 files covered. (100.0%)

928 of 1446 relevant lines covered (64.18%)

19.59 hits per line

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

82.61
/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.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"
69✔
62

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

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

84
      # Data was modified?
85
      @modified = false
69✔
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
69✔
90

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

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

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

100
      # Service names of the NTP daemon
101
      @service_name = "chronyd"
69✔
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"
69✔
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
69✔
112

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

116
      # Required packages
117
      @required_packages = [REQUIRED_PACKAGE]
69✔
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
69✔
126

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

130
      @config_has_been_read = false
69✔
131

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

136
      @random_pool_servers = RANDOM_POOL_NTP_SERVERS
69✔
137

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

142
    # CFA instance for reading/writing /etc/chrony.conf
143
    def ntp_conf
1✔
144
      @ntp_conf ||= CFA::ChronyConf.new
107✔
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,309✔
195
      # There is no gb.pool.ntp.org only uk.pool.ntp.org
196
      mycc = "uk" if mycc == "gb"
1,309✔
197
      {
198
        "address"  => "#{mycc}.pool.ntp.org",
1,309✔
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?
14✔
234

235
      deep_copy(@ntp_servers)
14✔
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?
11✔
242
        @country_names = Convert.convert(
11✔
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?
11✔
249
        Builtins.y2error("Failed to read country names")
×
250
        @country_names = {}
×
251
      end
252
      deep_copy(@country_names)
11✔
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
        servers.select! { |_server, attrs| attrs["country"] == country }
×
266
        # bnc#458917 add country, in case data/country.ycp does not have it
267
        pool_country_record = MakePoolRecord(country, "")
×
268
        servers[pool_country_record["address"]] = pool_country_record
×
269
      else
270
        country_names = GetCountryNames()
2✔
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)
3✔
290
        log.error("File #{NTP_FILE} does not exist")
2✔
291
        return false
2✔
292
      end
293

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

301
      true
1✔
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
4✔
308
        log.info "Configuration has been read already, skipping."
1✔
309
        return false
1✔
310
      end
311

312
      return false unless read_ntp_conf
3✔
313

314
      @config_has_been_read = true
1✔
315

316
      true
1✔
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)
7✔
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")
27✔
340

341
      return true if @config_has_been_read
27✔
342

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

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

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

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

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

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

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

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

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

372
      return false if !go_next
9✔
373

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

376
      return false if Abort()
9✔
377

378
      @modified = false
8✔
379
      true
8✔
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
2✔
387
    end
388

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

394
      sources
41✔
395
    end
396

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

403
      # write settings
404
      return false if !go_next
9✔
405

406
      Report.Error(Message.CannotWriteSettingsTo("/etc/chrony.conf")) if !write_ntp_conf
8✔
407

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

414
      # restart daemon
415
      return false if !go_next
8✔
416

417
      check_service
8✔
418

419
      update_timer_settings
8✔
420

421
      return false if !go_next
8✔
422

423
      Progress.Title(_("Finished")) if progress?
8✔
424

425
      !Abort()
8✔
426
    end
427

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

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

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

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

476
      true
11✔
477
    end
478

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

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

519
      result
1✔
520
    end
521

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

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

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

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

570
      output["exit"] == 0
7✔
571
    end
572

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

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

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

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

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

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

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

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

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

675
  private
1✔
676

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

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

712
      deep_copy(@known_countries)
15✔
713
    end
714

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

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

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

750
    # Set @ntp_servers with known servers and known countries pool ntp servers
751
    def update_ntp_servers!
1✔
752
      @ntp_servers = {}
17✔
753

754
      read_known_servers.each { |s| cache_server(s) }
2,530✔
755

756
      pool_servers_for(GetAllKnownCountries()).each { |p| cache_server(p) }
1,321✔
757
    end
758

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

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

807
    def update_cfa_record(record)
1✔
808
      cfa_record = record["cfa_record"]
×
809
      cfa_record.value = record["address"]
×
810
      cfa_record.raw_options = record["options"]
×
811
      cfa_record.comment = record["comment"]
×
812
    end
813

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

824
      true
×
825
    end
826

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

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

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

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

853
      success
×
854
    end
855

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

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

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

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

933
      return {} if !servers_file
15✔
934

935
      servers = YAML.load_file(servers_file)
15✔
936
      if servers.nil?
15✔
937
        log.error("Failed to read the list of NTP servers")
×
938
        return {}
×
939
      end
940

941
      log.info "Known NTP servers read: #{servers}"
15✔
942

943
      servers
15✔
944
    end
945

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

961
      " (#{location}#{country})"
64✔
962
    end
963

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

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

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

994
      displayinfo = Yast::UI.GetDisplayInfo
×
995
      width = displayinfo["TextMode"] ? displayinfo.fetch("Width", 80) : 80
×
996

997
      Yast::Report.Error(wrap_text(msg, width - 4))
×
998
    end
999

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

1010
  NtpClient = NtpClientClass.new
1✔
1011
  NtpClient.main
1✔
1012
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