syncwrap 2.7.1 → 2.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/History.rdoc +61 -0
  3. data/Manifest.txt +3 -0
  4. data/lib/syncwrap/base.rb +1 -1
  5. data/lib/syncwrap/change_key_listener.rb +56 -0
  6. data/lib/syncwrap/cli.rb +71 -8
  7. data/lib/syncwrap/components/arch.rb +8 -10
  8. data/lib/syncwrap/components/bundle.rb +3 -6
  9. data/lib/syncwrap/components/bundled_iyyov_daemon.rb +3 -8
  10. data/lib/syncwrap/components/bundler_gem.rb +2 -2
  11. data/lib/syncwrap/components/cruby_vm.rb +9 -5
  12. data/lib/syncwrap/components/debian.rb +13 -11
  13. data/lib/syncwrap/components/iyyov.rb +21 -4
  14. data/lib/syncwrap/components/jruby_vm.rb +5 -4
  15. data/lib/syncwrap/components/postgresql.rb +28 -7
  16. data/lib/syncwrap/components/puma.rb +112 -35
  17. data/lib/syncwrap/components/qpid.rb +2 -2
  18. data/lib/syncwrap/components/rhel.rb +65 -34
  19. data/lib/syncwrap/components/run_user.rb +11 -4
  20. data/lib/syncwrap/components/source_tree.rb +5 -1
  21. data/lib/syncwrap/components/tarpit_gem.rb +2 -2
  22. data/lib/syncwrap/components/users.rb +2 -1
  23. data/lib/syncwrap/context.rb +15 -2
  24. data/lib/syncwrap/distro.rb +5 -7
  25. data/lib/syncwrap/shell.rb +2 -2
  26. data/lib/syncwrap/systemd_service.rb +140 -0
  27. data/lib/syncwrap.rb +4 -3
  28. data/sync/etc/systemd/system/puma.service.erb +3 -0
  29. data/sync/etc/systemd/system/puma.socket.erb +15 -0
  30. data/sync/postgresql/postgresql.conf.erb +25 -5
  31. data/test/test_components.rb +14 -2
  32. data/test/test_context.rb +1 -1
  33. data/test/test_context_rput.rb +1 -1
  34. data/test/test_rsync.rb +1 -1
  35. data/test/test_shell.rb +2 -1
  36. data/test/test_space.rb +22 -1
  37. data/test/test_space_main.rb +6 -2
  38. data/test/test_version_support.rb +1 -1
  39. data/test/test_zone_balancer.rb +2 -2
  40. metadata +7 -4
@@ -15,18 +15,23 @@
15
15
  #++
16
16
 
17
17
  require 'syncwrap/component'
18
+ require 'syncwrap/change_key_listener'
19
+ require 'syncwrap/systemd_service'
18
20
 
19
21
  module SyncWrap
20
22
 
21
- # Provision to install, start/restart a Puma HTTP
22
- # server, optionally triggered by a state change key.
23
+ # Provision to install and start/restart a Puma HTTP server
24
+ # instance, optionally triggered by a state change key. Systemd
25
+ # service and socket units are supported.
23
26
  #
24
- # Host component dependencies: RunUser, <ruby>
27
+ # Host component dependencies: <Distro>?, RunUser, <ruby>, SourceTree?
25
28
  class Puma < Component
29
+ include ChangeKeyListener
30
+ include SystemDService
26
31
 
27
32
  # Puma version to install and run, if set. Otherwise assume puma
28
33
  # is bundled with the application (i.e. Bundle) and use bin stubs
29
- # to run. (Default: nil; Example: 2.9.0)
34
+ # to run. (Default: nil; Example: 3.3.0)
30
35
  attr_accessor :puma_version
31
36
 
32
37
  # Path to the application/configuration directory which
@@ -48,16 +53,11 @@ module SyncWrap
48
53
  state: "#{rack_path}/puma.state",
49
54
  control: "unix://#{rack_path}/control",
50
55
  environment: "production",
51
- port: 5874,
52
56
  daemon: !foreground? }.merge( @puma_flags )
53
57
  end
54
58
 
55
59
  protected
56
60
 
57
- # An optional state key to check, indicating changes requiring
58
- # a Puma restart (Default: nil; Example: :source_tree)
59
- attr_accessor :change_key
60
-
61
61
  # Should Puma be restarted even when there were no source bundle
62
62
  # changes? (Default: false)
63
63
  attr_writer :always_restart
@@ -66,21 +66,40 @@ module SyncWrap
66
66
  @always_restart
67
67
  end
68
68
 
69
- # The name of the systemd unit file to create for this instance of
70
- # puma. If specified, the name should include a '.service' suffix.
71
- # (Default: nil -> no unit)
72
- attr_accessor :systemd_unit
69
+ # Deprecated
70
+ alias :systemd_unit :systemd_service
71
+
72
+ # Deprecated
73
+ alias :systemd_unit= :systemd_service=
74
+
75
+ # An array of ListenStream configuration values for
76
+ # #systemd_socket. If a #puma_flags[:port] is specified, this
77
+ # defaults to a single '0.0.0.0:port' stream. Otherwise this
78
+ # setting is required if #systemd_socket is specified.
79
+ attr_writer :listen_streams
80
+
81
+ def listen_streams
82
+ if @listen_streams
83
+ @listen_streams
84
+ elsif p = puma_flags[:port]
85
+ [ "0.0.0.0:#{p}" ]
86
+ else
87
+ raise( "Neither #listen_streams nor #puma_flags[:port] specified" +
88
+ " with Puma#systemd_socket" )
89
+ end
90
+ end
73
91
 
74
92
  public
75
93
 
76
94
  def initialize( opts = {} )
77
95
  @puma_version = nil
78
96
  @always_restart = false
79
- @change_key = nil
80
97
  @rack_path = nil
81
98
  @puma_flags = {}
82
- @systemd_unit = nil
83
99
  super
100
+ if systemd_socket && !systemd_service
101
+ raise "Puma#systemd_service is required when #systemd_socket is specified"
102
+ end
84
103
  end
85
104
 
86
105
  def install
@@ -88,46 +107,78 @@ module SyncWrap
88
107
  gem_install( 'puma', version: puma_version )
89
108
  end
90
109
 
91
- changes = change_key && state[ change_key ]
92
-
93
- if systemd_unit
94
- uchanges = rput( '/etc/systemd/system/puma.service',
95
- "/etc/systemd/system/#{systemd_unit}",
96
- user: :root )
97
- if !uchanges.empty?
98
- systemctl( 'enable', systemd_unit )
99
- end
100
- if( change_key.nil? || changes || uchanges || always_restart? )
101
- systemctl( 'restart', systemd_unit )
102
- else
103
- systemctl( 'start', systemd_unit )
104
- end
110
+ if systemd_service
111
+ install_units( always_restart? || change_key_changes? )
105
112
  else
106
113
  rudo( "( cd #{rack_path}", close: ')' ) do
107
114
  rudo( "if [ -f puma.state -a -e control ]; then",
108
115
  close: bare_else_start ) do
109
- if ( change_key && !changes ) && !always_restart?
110
- rudo 'true' #no-op
111
- else
116
+ if always_restart? || change_key_changes?
112
117
  bare_restart
118
+ else
119
+ rudo 'true' #no-op
113
120
  end
114
121
  end
115
122
  end
123
+ nil
124
+ end
125
+ end
126
+
127
+ def start
128
+ if systemd_service
129
+ super
130
+ else
131
+ bare_start
132
+ end
133
+ end
134
+
135
+ def restart( *args )
136
+ if systemd_service
137
+ super
138
+ else
139
+ bare_restart
140
+ end
141
+ end
142
+
143
+ def stop
144
+ if systemd_service
145
+ super
146
+ else
147
+ bare_stop
148
+ end
149
+ end
150
+
151
+ def status
152
+ if systemd_service
153
+ super
154
+ else
155
+ bare_status
116
156
  end
117
- nil
118
157
  end
119
158
 
120
159
  protected
121
160
 
122
- # By default, runs in foreground if a systemd_unit is specified.
161
+ # By default, runs in foreground if a systemd_service is specified.
123
162
  def foreground?
124
- !!systemd_unit
163
+ !!systemd_service
125
164
  end
126
165
 
127
166
  def bare_restart
128
167
  rudo( ( pumactl_command + %w[ --state puma.state restart ] ).join( ' ' ) )
129
168
  end
130
169
 
170
+ def bare_stop
171
+ rudo( ( pumactl_command + %w[ --state puma.state stop ] ).join( ' ' ) )
172
+ end
173
+
174
+ def bare_status
175
+ rudo( ( pumactl_command + %w[ --state puma.state status ] ).join( ' ' ) )
176
+ end
177
+
178
+ def bare_start
179
+ rudo( "cd #{rack_path} && #{puma_start_command}" )
180
+ end
181
+
131
182
  def bare_else_start
132
183
  <<-SH
133
184
  else
@@ -171,5 +222,31 @@ module SyncWrap
171
222
  '--' + key.to_s.gsub( /_/, '-' )
172
223
  end
173
224
 
225
+ def rput_unit_files
226
+ c = rput( src_for_systemd_service,
227
+ "/etc/systemd/system/#{systemd_service}", user: :root )
228
+ if systemd_socket
229
+ c += rput( src_for_systemd_socket,
230
+ "/etc/systemd/system/#{systemd_socket}", user: :root )
231
+ end
232
+ c
233
+ end
234
+
235
+ def src_for_systemd_service
236
+ s = "/etc/systemd/system/#{systemd_service}"
237
+ unless find_source( s )
238
+ s = '/etc/systemd/system/puma.service'
239
+ end
240
+ s
241
+ end
242
+
243
+ def src_for_systemd_socket
244
+ s = "/etc/systemd/system/#{systemd_socket}"
245
+ unless find_source( s )
246
+ s = '/etc/systemd/system/puma.socket'
247
+ end
248
+ s
249
+ end
250
+
174
251
  end
175
252
  end
@@ -131,7 +131,7 @@ module SyncWrap
131
131
 
132
132
  def corosync_install!( opts = {} )
133
133
  corosync_build
134
- dist_install( "#{corosync_src}/x86_64/*.rpm", succeed: true )
134
+ dist_install( "#{corosync_src}/x86_64/*.rpm" )
135
135
  end
136
136
 
137
137
  def qpid_tools_install!
@@ -287,7 +287,7 @@ module SyncWrap
287
287
  cd /tmp/rpm-drop
288
288
  #{curls.join("\n")}
289
289
  SH
290
- dist_install( "/tmp/rpm-drop/*.rpm", succeed: true )
290
+ dist_install( "/tmp/rpm-drop/*.rpm" )
291
291
  end
292
292
 
293
293
  protected
@@ -33,67 +33,98 @@ module SyncWrap
33
33
 
34
34
  alias :distro_version :rhel_version
35
35
 
36
+ protected
37
+
38
+ # Set true/false to override the default, distro version based
39
+ # determination of whether systemd is PID 1 on the system.
40
+ attr_writer :systemd
41
+
42
+ public
43
+
36
44
  def initialize( opts = {} )
37
45
  super
38
46
  end
39
47
 
40
48
  def systemd?
41
- @is_systemd ||= version_gte?( rhel_version, [7] )
49
+ if @systemd.nil?
50
+ @systemd = version_gte?( rhel_version, [7] )
51
+ end
52
+ @systemd
42
53
  end
43
54
 
44
- # Install the specified package names. A trailing hash is
45
- # interpreted as options, see below.
55
+ # Install the specified packages. When rpm HTTP URLs or local file
56
+ # paths are given instead of package names, these are installed
57
+ # first and individually via #dist_install_url. Calling that
58
+ # explicitly may be preferable. A trailing hash is interpreted as
59
+ # options, see below.
46
60
  #
47
61
  # ==== Options
48
62
  #
49
- # :check_install:: Short-circuit if all packages already
50
- # installed. Thus no upgrades will be performed.
63
+ # :check_install:: Short-circuit if packages are already
64
+ # installed, and thus don't perform updates
65
+ # unless versions are specified. (Default: true)
51
66
  #
52
- # :succeed:: Deprecated, use check_install instead
67
+ # :yum_flags:: Additional array of flags to pass to `yum install`.
53
68
  #
54
- # Additional options are passed to the sudo calls.
69
+ # Options are also passed to the sudo calls.
55
70
  def dist_install( *pkgs )
56
- opts = pkgs.last.is_a?( Hash ) && pkgs.pop.dup || {}
57
- opts.delete( :minimal )
58
- pkgs.flatten!
59
- chk = opts.delete( :check_install )
60
- chk = opts.delete( :succeed ) if chk.nil?
71
+ opts = pkgs.last.is_a?( Hash ) && pkgs.pop || {}
72
+ chk = opts[ :check_install ]
61
73
  chk = check_install? if chk.nil?
62
- dist_if_not_installed?( pkgs, chk, opts ) do
63
- sudo( "yum install -q -y #{pkgs.join( ' ' )}", opts )
74
+ flags = Array( opts[ :yum_flags ] )
75
+ pkgs.flatten!
76
+ rpms,names = pkgs.partition { |p| p =~ /\.rpm$/ || p =~ /^http(s)?:/i }
77
+ rpms.each do |url|
78
+ dist_install_url( url, nil, opts )
79
+ end
80
+ !names.empty? && dist_if_not_installed?( names, chk != false, opts ) do
81
+ sudo( "yum install -q -y #{(flags + names).join ' '}", opts )
64
82
  end
65
83
  end
66
84
 
67
- # Uninstall the specified package names. A trailing hash is
68
- # interpreted as options, see below.
85
+ # Install packages by HTTP URL or local file path to rpm. Uses
86
+ # name to check_install. If not specified, name is deduced via
87
+ # `File.basename( url, '.rpm' )`. It is not recommended to set
88
+ # option `check_install: false`, because `yum` will fail with
89
+ # "Error: Nothing to do" if given a file/URL and the package is
90
+ # already installed.
69
91
  #
70
92
  # ==== Options
71
93
  #
72
- # :succeed:: Succeed even if none of the packages are
73
- # installed. (Deprecated, Default: true)
94
+ # :check_install:: Short-circuit if package is already
95
+ # installed. (Default: true)
74
96
  #
75
- # Additional options are passed to the sudo calls.
97
+ # :yum_flags:: Additional array of flags to pass to `yum install`.
98
+ #
99
+ # Options are also passed to the sudo calls.
100
+ def dist_install_url( url, name = nil, opts = {} )
101
+ name ||= File.basename( url, '.rpm' )
102
+ chk = opts[ :check_install ]
103
+ flags = Array( opts[ :yum_flags ] )
104
+ dist_if_not_installed?( name, chk != false, opts ) do
105
+ sudo( "yum install -q -y #{(flags + [url]).join ' '}", opts )
106
+ end
107
+ end
108
+
109
+ # Uninstall the specified package names. A trailing hash is
110
+ # interpreted as options. These are passed to the sudo.
76
111
  def dist_uninstall( *pkgs )
77
- opts = pkgs.last.is_a?( Hash ) && pkgs.pop.dup || {}
112
+ opts = pkgs.last.is_a?( Hash ) && pkgs.pop || {}
78
113
  pkgs.flatten!
79
- if opts.delete( :succeed ) != false
80
- sudo( <<-SH, opts )
81
- if yum list -C -q installed #{pkgs.join( ' ' )} >/dev/null 2>&1; then
82
- yum remove -q -y #{pkgs.join( ' ' )}
83
- fi
84
- SH
85
- else
86
- sudo( "yum remove -q -y #{pkgs.join( ' ' )}", opts )
87
- end
114
+ sudo( <<-SH, opts )
115
+ if yum list -C -q installed #{pkgs.join ' '} >/dev/null 2>&1; then
116
+ yum remove -q -y #{pkgs.join ' '}
117
+ fi
118
+ SH
88
119
  end
89
120
 
90
- # If chk is true, then wrap block in a sudo bash conditional
91
- # testing if any specified pkgs are not installed. Otherwise just
121
+ # If chk is true, then wrap block in a sudo bash conditional that tests
122
+ # if any specified pkgs are not installed. Otherwise just
92
123
  # yield to block.
93
124
  def dist_if_not_installed?( pkgs, chk, opts, &block )
94
- if chk && pkgs.all? { |p| p !~ /\.rpm$/ && p !~ /^http(s)?:/ }
95
- qry = "yum list -C -q installed #{pkgs.join ' '}"
96
- cnt = qry + " | tail -n +2 | wc -l"
125
+ if chk
126
+ pkgs = Array( pkgs )
127
+ cnt = "rpm -q #{pkgs.join ' '} | grep -cv 'not installed'"
97
128
  cond = %Q{if [ "$(#{cnt})" != "#{pkgs.count}" ]; then}
98
129
  sudo( cond, opts.merge( close: 'fi' ), &block )
99
130
  else
@@ -36,6 +36,9 @@ module SyncWrap
36
36
  @run_dir || "/var/local/#{run_user}"
37
37
  end
38
38
 
39
+ # File mode as integer for the #run_dir (default: 0755)
40
+ attr_accessor :run_dir_mode
41
+
39
42
  # Home directory for the #run_user
40
43
  # (default: nil -> same as #run_dir)
41
44
  attr_writer :run_user_home
@@ -48,6 +51,7 @@ module SyncWrap
48
51
  @run_user = 'runr'
49
52
  @run_group = nil
50
53
  @run_dir = nil
54
+ @run_dir_mode = 0755
51
55
  @run_user_home = nil
52
56
  super
53
57
  end
@@ -73,7 +77,7 @@ module SyncWrap
73
77
  # Create and set owner/permission of run_dir, such that run_user may
74
78
  # create new directories there.
75
79
  def create_run_dir
76
- mkdir_run_user( run_dir )
80
+ mkdir_run_user( run_dir, mode: run_dir_mode )
77
81
  end
78
82
 
79
83
  def service_dir( sname, instance = nil )
@@ -87,11 +91,14 @@ module SyncWrap
87
91
  mkdir_run_user( sdir )
88
92
  end
89
93
 
90
- # Make dir including parents, chown to run_user and chmod 775.
91
- def mkdir_run_user( dir )
94
+ # Make dir including parents via sudo, chown to run_user, and chmod
95
+ # === Options
96
+ # :mode:: Integer file mode for directory set via chmod (Default: 0775)
97
+ def mkdir_run_user( dir, opts = {} )
98
+ mode = opts[:mode] || 0775
92
99
  sudo "mkdir -p #{dir}"
93
100
  chown_run_user dir
94
- sudo "chmod 775 #{dir}"
101
+ sudo( "chmod %o %s" % [ mode, dir ] )
95
102
  end
96
103
 
97
104
  # Deprecated
@@ -53,6 +53,9 @@ module SyncWrap
53
53
  @remote_dir || source_dir
54
54
  end
55
55
 
56
+ # File mode as integer for the #remote_dir (Default: 0755)
57
+ attr_accessor :remote_dir_mode
58
+
56
59
  protected
57
60
 
58
61
  # Require local source_dir to be clean per Git before #rput of the
@@ -84,6 +87,7 @@ module SyncWrap
84
87
  @local_source_root = path_relative_to_caller( '..', clr )
85
88
  @source_dir = nil
86
89
  @remote_dir = nil
90
+ @remote_dir_mode = 0755
87
91
  @remote_source_root = nil
88
92
  @require_clean = true
89
93
  @rput_options = {}
@@ -99,7 +103,7 @@ module SyncWrap
99
103
  end
100
104
 
101
105
  def install
102
- mkdir_run_user( remote_source_path )
106
+ mkdir_run_user( remote_source_path, mode: remote_dir_mode )
103
107
  changes = sync_source
104
108
  on_change( changes ) unless changes.empty?
105
109
  changes
@@ -24,7 +24,7 @@ module SyncWrap
24
24
  #
25
25
  class TarpitGem < Component
26
26
 
27
- # Tarpit version to install (Default: 1.1.0)
27
+ # Tarpit version to install (Default: 2.1.1)
28
28
  attr_accessor :tarpit_version
29
29
 
30
30
  protected
@@ -39,7 +39,7 @@ module SyncWrap
39
39
  public
40
40
 
41
41
  def initialize( opts = {} )
42
- @tarpit_version = '2.1.0'
42
+ @tarpit_version = '2.1.1'
43
43
  @user_install = false
44
44
  super
45
45
  end
@@ -197,7 +197,8 @@ module SyncWrap
197
197
  flags[ :ssh_user ] = ssh_user
198
198
  if ssh_user_pem
199
199
  flags[ :ssh_user_pem ] = ssh_user_pem
200
- flags[ :ssh_options ] = { 'IdentitiesOnly' => 'yes' }
200
+ flags[ :ssh_options ] = { 'IdentitiesOnly' => 'yes',
201
+ 'PasswordAuthentication' => 'no' }
201
202
  end
202
203
  end
203
204
  flags
@@ -93,7 +93,7 @@ module SyncWrap
93
93
  opts = @default_options.merge( opts )
94
94
  close = opts.delete( :close )
95
95
 
96
- flush if opts != @queued_opts #may still be a no-op
96
+ flush unless sh_opts_equal?( @queued_opts, opts ) #may still be a no-op
97
97
 
98
98
  @queued_cmd << command
99
99
  @queued_opts = opts
@@ -185,6 +185,19 @@ module SyncWrap
185
185
 
186
186
  private
187
187
 
188
+ SH_OPT_KEYS = [ :accept, :coalesce, :dryrun, :error, :pipefail,
189
+ :sh_verbose, :ssh_flags, :ssh_options, :ssh_user,
190
+ :ssh_user_pem, :sudo_flags, :user, :verbose ].freeze
191
+
192
+ # Compare options hashes for equality, but only keys that
193
+ # influence execution of #sh, ignoring others.
194
+ def sh_opts_equal?( o1, o2 )
195
+ SH_OPT_KEYS.each do |k|
196
+ return false if o1[k] != o2[k]
197
+ end
198
+ true
199
+ end
200
+
188
201
  def ssh_host_name
189
202
  host.space.ssh_host_name( host )
190
203
  end
@@ -207,7 +220,7 @@ module SyncWrap
207
220
 
208
221
  def rsync( srcs, target, opts )
209
222
  args = rsync_args( ssh_host_name, srcs, target, opts )
210
- exit_code, outputs = capture_stream( args, host, :rsync, opts )
223
+ _,outputs = capture_stream( args, host, :rsync, opts )
211
224
 
212
225
  # Return array of --itemize-changes on standard out.
213
226
  collect_stream( :out, outputs ).
@@ -41,20 +41,19 @@ module SyncWrap
41
41
  #
42
42
  # ==== Options
43
43
  #
44
- # :check_install:: Short-circuit if all packages already
45
- # installed. Thus no upgrades will be performed.
46
- #
47
- # :succeed:: Deprecated, use check_install instead
44
+ # :check_install:: Short-circuit if packages are already
45
+ # installed. Thus no upgrades will be
46
+ # performed. (Default: true)
48
47
  #
49
48
  # :minimal:: Avoid additional "optional" packages when possible
50
49
  #
51
- # Additional options are passed to the sudo calls.
50
+ # Options are also passed to the sudo calls.
52
51
  def dist_install( *pkgs )
53
52
  raise "Include a distro-specific component, e.g. Debian, RHEL"
54
53
  end
55
54
 
56
55
  # Uninstall the specified package names. A trailing hash is
57
- # interpreted as options, passed to the sudo calls.
56
+ # interpreted as options and passed to the sudo calls.
58
57
  def dist_uninstall( *pkgs )
59
58
  raise "Include a distro-specific component, e.g. Debian, RHEL"
60
59
  end
@@ -67,7 +66,6 @@ module SyncWrap
67
66
  # systemctl( 'enable', 'name.service' )
68
67
  #
69
68
  # See #systemd?, SystemD#systemctl
70
- #
71
69
  def dist_install_init_service( name )
72
70
  raise "Include a distro-specific component, e.g. Debian, RHEL"
73
71
  end
@@ -98,7 +98,7 @@ module SyncWrap
98
98
 
99
99
  if opts[ :coalesce ]
100
100
  args << '-c'
101
- cmd = "exec 1>&2\n"
101
+ cmd = String.new( "exec 1>&2\n" )
102
102
  if opts[ :sh_verbose ]
103
103
  cmd << "set " << ( opts[ :sh_verbose ] == :x ? '-x' : '-v' ) << "\n"
104
104
  end
@@ -110,7 +110,7 @@ module SyncWrap
110
110
  args << ( opts[ :sh_verbose ] == :x ? '-x' : '-v' )
111
111
  end
112
112
  args << '-c'
113
- cmd = ""
113
+ cmd = String.new
114
114
  cmd << "cd /\n" if opts[:user]
115
115
  cmd << command_lines_cleanup( command )
116
116
  args << cmd