puppet 4.9.3-universal-darwin → 4.9.4-universal-darwin

Sign up to get free protection for your applications and to get access to all the features.

Potentially problematic release.


This version of puppet might be problematic. Click here for more details.

@@ -28,21 +28,29 @@ Puppet::Functions.create_function(:eyaml_lookup_key) do
28
28
  context.explain { "Setting Eyaml option '#{k}' to '#{v}'" }
29
29
  end
30
30
  end
31
- raw_data = load_data_hash(options)
31
+ raw_data = load_data_hash(options, context)
32
32
  context.cache(nil, raw_data)
33
33
  end
34
34
  context.not_found unless raw_data.include?(key)
35
35
  context.cache(key, decrypt_value(raw_data[key], context))
36
36
  end
37
37
 
38
- def load_data_hash(options)
39
- begin
40
- data = YAML.load_file(options['path'])
41
- Puppet::Pops::Lookup::HieraConfig.symkeys_to_string(data.is_a?(Hash) ? data : {})
42
- rescue YAML::SyntaxError => ex
43
- # Psych errors includes the absolute path to the file, so no need to add that
44
- # to the message
45
- raise Puppet::DataBinding::LookupError, "Unable to parse #{ex.message}"
38
+ def load_data_hash(options, context)
39
+ path = options['path']
40
+ context.cached_file_data(path) do |content|
41
+ begin
42
+ data = YAML.load(content, path)
43
+ if data.is_a?(Hash)
44
+ Puppet::Pops::Lookup::HieraConfig.symkeys_to_string(data)
45
+ else
46
+ Puppet.warning("#{path}: file does not contain a valid yaml hash")
47
+ {}
48
+ end
49
+ rescue YAML::SyntaxError => ex
50
+ # Psych errors includes the absolute path to the file, so no need to add that
51
+ # to the message
52
+ raise Puppet::DataBinding::LookupError, "Unable to parse #{ex.message}"
53
+ end
46
54
  end
47
55
  end
48
56
 
@@ -52,7 +60,7 @@ Puppet::Functions.create_function(:eyaml_lookup_key) do
52
60
  decrypt(value, context)
53
61
  when Hash
54
62
  result = {}
55
- value.each_pair { |k, v| result[k] = decrypt_value(v, context) }
63
+ value.each_pair { |k, v| result[context.interpolate(k)] = decrypt_value(v, context) }
56
64
  result
57
65
  when Array
58
66
  value.map { |v| decrypt_value(v, context) }
@@ -15,10 +15,12 @@ Puppet::Functions.create_function(:hocon_data) do
15
15
 
16
16
  def hocon_data(options, context)
17
17
  path = options['path']
18
- begin
19
- Hocon.parse(Puppet::FileSystem.read(path, :encoding => 'utf-8'))
20
- rescue Hocon::ConfigError => ex
21
- raise Puppet::DataBinding::LookupError, "Unable to parse (#{path}): #{ex.message}"
18
+ context.cached_file_data(path) do |content|
19
+ begin
20
+ Hocon.parse(content)
21
+ rescue Hocon::ConfigError => ex
22
+ raise Puppet::DataBinding::LookupError, "Unable to parse (#{path}): #{ex.message}"
23
+ end
22
24
  end
23
25
  end
24
26
  end
@@ -8,11 +8,13 @@ Puppet::Functions.create_function(:json_data) do
8
8
 
9
9
  def json_data(options, context)
10
10
  path = options['path']
11
- begin
12
- JSON.parse(Puppet::FileSystem.read(path, :encoding => 'utf-8'))
13
- rescue JSON::ParserError => ex
14
- # Filename not included in message, so we add it here.
15
- raise Puppet::DataBinding::LookupError, "Unable to parse (#{path}): #{ex.message}"
11
+ context.cached_file_data(path) do |content|
12
+ begin
13
+ JSON.parse(content)
14
+ rescue JSON::ParserError => ex
15
+ # Filename not included in message, so we add it here.
16
+ raise Puppet::DataBinding::LookupError, "Unable to parse (#{path}): #{ex.message}"
17
+ end
16
18
  end
17
19
  end
18
20
  end
@@ -9,18 +9,21 @@ Puppet::Functions.create_function(:yaml_data) do
9
9
  end
10
10
 
11
11
  def yaml_data(options, context)
12
- begin
13
- path = options['path']
14
- data = YAML.load_file(path)
15
- unless data.is_a?(Hash)
16
- Puppet.warning("#{path}: file does not contain a valid yaml hash")
17
- data = {}
12
+ path = options['path']
13
+ context.cached_file_data(path) do |content|
14
+ begin
15
+ data = YAML.load(content, path)
16
+ if data.is_a?(Hash)
17
+ Puppet::Pops::Lookup::HieraConfig.symkeys_to_string(data)
18
+ else
19
+ Puppet.warning("#{path}: file does not contain a valid yaml hash")
20
+ {}
21
+ end
22
+ rescue YAML::SyntaxError => ex
23
+ # Psych errors includes the absolute path to the file, so no need to add that
24
+ # to the message
25
+ raise Puppet::DataBinding::LookupError, "Unable to parse #{ex.message}"
18
26
  end
19
- Puppet::Pops::Lookup::HieraConfig.symkeys_to_string(data.nil? ? {} : data)
20
- rescue YAML::SyntaxError => ex
21
- # Psych errors includes the absolute path to the file, so no need to add that
22
- # to the message
23
- raise Puppet::DataBinding::LookupError, "Unable to parse #{ex.message}"
24
27
  end
25
28
  end
26
29
  end
@@ -28,15 +28,14 @@ class Puppet::Pops::Functions::Dispatcher
28
28
  #
29
29
  # @api private
30
30
  def dispatch(instance, calling_scope, args, &block)
31
- tc = Puppet::Pops::Types::TypeCalculator.singleton
32
- actual = tc.infer_set(block_given? ? args + [block] : args)
33
- found = @dispatchers.find { |d| tc.callable?(d.type, actual) }
34
- if found
35
- catch(:next) do
36
- found.invoke(instance, calling_scope, args, &block)
37
- end
38
- else
39
- raise ArgumentError, Puppet::Pops::Types::TypeMismatchDescriber.describe_signatures(instance.class.name, @dispatchers, actual)
31
+ found = @dispatchers.find { |d| d.type.callable_with?(args, block) }
32
+ unless found
33
+ args_type = Puppet::Pops::Types::TypeCalculator.singleton.infer_set(block_given? ? args + [block] : args)
34
+ raise ArgumentError, Puppet::Pops::Types::TypeMismatchDescriber.describe_signatures(instance.class.name, @dispatchers, args_type)
35
+ end
36
+
37
+ catch(:next) do
38
+ found.invoke(instance, calling_scope, args, &block)
40
39
  end
41
40
  end
42
41
 
@@ -73,7 +73,9 @@ module Lookup
73
73
 
74
74
  # @api private
75
75
  def self.search_and_merge(name, lookup_invocation, merge)
76
- lookup_invocation.lookup_adapter.lookup(name, lookup_invocation, merge)
76
+ answer = lookup_invocation.lookup_adapter.lookup(name, lookup_invocation, merge)
77
+ lookup_invocation.emit_debug_info("Automatic Parameter Lookup of '#{name}") if Puppet[:debug]
78
+ answer
77
79
  end
78
80
 
79
81
  def self.assert_type(subject, type, value)
@@ -13,7 +13,7 @@ class ConfiguredDataProvider
13
13
  end
14
14
 
15
15
  def config(lookup_invocation)
16
- @config ||= assert_config_version(HieraConfig.create(configuration_path(lookup_invocation)))
16
+ @config ||= assert_config_version(HieraConfig.create(lookup_invocation, configuration_path(lookup_invocation)))
17
17
  end
18
18
 
19
19
  # @return [Pathname] the path to the configuration
@@ -2,19 +2,73 @@ require_relative 'interpolation'
2
2
 
3
3
  module Puppet::Pops
4
4
  module Lookup
5
+ # The EnvironmentContext is adapted to the current environment
6
+ #
7
+ class EnvironmentContext < Adaptable::Adapter
8
+ class FileData
9
+ attr_reader :data
10
+
11
+ def initialize(path, inode, mtime, size, data)
12
+ @path = path
13
+ @inode = inode
14
+ @mtime = mtime
15
+ @size = size
16
+ @data = data
17
+ end
18
+
19
+ def valid?(stat)
20
+ stat.ino == @inode && stat.mtime == @mtime && stat.size == @size
21
+ end
22
+ end
23
+
24
+ attr_reader :environment_name
25
+
26
+ def self.create_adapter(environment)
27
+ new(environment)
28
+ end
29
+
30
+ def initialize(environment)
31
+ @environment_name = environment.name
32
+ @file_data_cache = {}
33
+ end
34
+
35
+ # Loads the contents of the file given by _path_. The content is then yielded to the provided block in
36
+ # case a block is given, and the returned value from that block is cached and returned by this method.
37
+ # If no block is given, the content is stored instead.
38
+ #
39
+ # The cache is retained as long as the inode, mtime, and size of the file remains unchanged.
40
+ #
41
+ # @param path [String] path to the file to be read
42
+ # @yieldparam content [String] the content that was read from the file
43
+ # @yieldreturn [Object] some result based on the content
44
+ # @return [Object] the content, or if a block was given, the return value of the block
45
+ #
46
+ def cached_file_data(path)
47
+ file_data = @file_data_cache[path]
48
+ stat = Puppet::FileSystem.stat(path)
49
+ unless file_data && file_data.valid?(stat)
50
+ Puppet.debug("File at '#{path}' was changed, reloading") if file_data
51
+ content = Puppet::FileSystem.read(path, :encoding => 'utf-8')
52
+ file_data = FileData.new(path, stat.ino, stat.mtime, stat.size, block_given? ? yield(content) : content)
53
+ @file_data_cache[path] = file_data
54
+ end
55
+ file_data.data
56
+ end
57
+ end
58
+
5
59
  # A FunctionContext is created for each unique hierarchy entry and adapted to the Compiler (and hence shares
6
60
  # the compiler's life-cycle).
7
61
  # @api private
8
62
  class FunctionContext
9
63
  include Interpolation
10
64
 
11
- attr_reader :environment_name, :module_name, :function
65
+ attr_reader :module_name, :function
12
66
  attr_accessor :data_hash
13
67
 
14
- def initialize(environment_name, module_name, function)
68
+ def initialize(environment_context, module_name, function)
15
69
  @data_hash = nil
16
70
  @cache = {}
17
- @environment_name = environment_name
71
+ @environment_context = environment_context
18
72
  @module_name = module_name
19
73
  @function = function
20
74
  end
@@ -51,6 +105,14 @@ class FunctionContext
51
105
  Types::Iterable.on(@cache)
52
106
  end
53
107
  end
108
+
109
+ def cached_file_data(path, &block)
110
+ @environment_context.cached_file_data(path, &block)
111
+ end
112
+
113
+ def environment_name
114
+ @environment_context.environment_name
115
+ end
54
116
  end
55
117
 
56
118
  # The Context is created once for each call to a function. It provides a combination of the {Invocation} object needed
@@ -73,10 +135,12 @@ class Context
73
135
  key_type = tf.optional(tf.scalar)
74
136
  @type = Pcore::create_object_type(loader, ir, self, 'Puppet::LookupContext', 'Any',
75
137
  {
76
- 'environment_name' => Types::PStringType::NON_EMPTY,
138
+ 'environment_name' => {
139
+ Types::KEY_TYPE => Types::PStringType::NON_EMPTY,
140
+ Types::KEY_KIND => Types::PObjectType::ATTRIBUTE_KIND_DERIVED
141
+ },
77
142
  'module_name' => {
78
- Types::KEY_TYPE => tf.optional(Types::PStringType::NON_EMPTY),
79
- Types::KEY_VALUE => nil
143
+ Types::KEY_TYPE => tf.variant(Types::PStringType::NON_EMPTY, Types::PUndefType::DEFAULT)
80
144
  }
81
145
  },
82
146
  {
@@ -90,19 +154,19 @@ class Context
90
154
  'cached_entries' => tf.variant(
91
155
  tf.callable([0, 0, tf.callable(1,1)], tf.undef),
92
156
  tf.callable([0, 0, tf.callable(2,2)], tf.undef),
93
- tf.callable([0, 0], tf.iterable(tf.tuple([key_type, tf.any])))
94
- )
157
+ tf.callable([0, 0], tf.iterable(tf.tuple([key_type, tf.any])))),
158
+ 'cached_file_data' => tf.callable(tf.string, tf.optional(tf.callable([1, 1])))
95
159
  }
96
160
  ).resolve(Types::TypeParser.singleton, loader)
97
161
  end
98
162
 
99
163
  # Mainly for test purposes. Makes it possible to create a {Context} in Puppet code provided that a current {Invocation} exists.
100
- def self.from_asserted_args(environment_name, module_name)
101
- new(FunctionContext.new(environment_name, module_name, nil), Invocation.current)
164
+ def self.from_asserted_args(module_name)
165
+ new(FunctionContext.new(EnvironmentContext.adapt(Puppet.lookup(:environments).get(Puppet[:environment])), module_name, nil), Invocation.current)
102
166
  end
103
167
 
104
168
  # Public methods delegated to the {FunctionContext}
105
- def_delegators :@function_context, :cache, :cache_all, :cache_has_key, :cached_value, :cached_entries, :environment_name, :module_name
169
+ def_delegators :@function_context, :cache, :cache_all, :cache_has_key, :cached_value, :cached_entries, :environment_name, :module_name, :cached_file_data
106
170
 
107
171
  def initialize(function_context, lookup_invocation)
108
172
  @lookup_invocation = lookup_invocation
@@ -54,7 +54,7 @@ class DataHashFunctionProvider < FunctionProvider
54
54
  lookup_invocation.report_not_found(root_key)
55
55
  throw :no_such_key
56
56
  end
57
- interpolate(value, lookup_invocation, true)
57
+ interpolate(parent_data_provider.validate_data_value(self, value), lookup_invocation, true)
58
58
  end
59
59
 
60
60
  def data_hash(lookup_invocation, location)
@@ -70,32 +70,23 @@ module DataProvider
70
70
  raise NotImplementedError, "Subclass of #{DataProvider.name} must implement 'name' method"
71
71
  end
72
72
 
73
- # Asserts that _data_hash_ is a valid hash.
73
+ # Asserts that _data_hash_ is a hash.
74
74
  #
75
75
  # @param data_provider [DataProvider] The data provider that produced the hash
76
76
  # @param data_hash [Hash{String=>Object}] The data hash
77
77
  # @return [Hash{String=>Object}] The data hash
78
78
  def validate_data_hash(data_provider, data_hash)
79
79
  Types::TypeAsserter.assert_instance_of(nil, Types::PHashType::DEFAULT, data_hash) { "Value returned from #{data_provider.name}" }
80
- data_hash.each_pair { |k, v| validate_data_entry(data_provider, k, v) }
81
- data_hash
82
80
  end
83
81
 
84
- def validate_data_value(data_provider, value, where = '')
85
- Types::TypeAsserter.assert_instance_of(nil, DataProvider.value_type, value) { "Value #{where}returned from #{data_provider.name}" }
86
- case value
87
- when Hash
88
- value.each_pair { |k, v| validate_data_entry(data_provider, k, v) }
89
- when Array
90
- value.each {|v| validate_data_value(data_provider, v, 'in array ') }
91
- end
92
- value
93
- end
94
-
95
- def validate_data_entry(data_provider, key, value)
96
- Types::TypeAsserter.assert_instance_of(nil, DataProvider.key_type, key) { "Key in hash returned from #{data_provider.name}" }
97
- validate_data_value(data_provider, value, 'in hash ')
98
- nil
82
+ # Asserts that _data_value_ is of valid type.
83
+ #
84
+ # @param data_provider [DataProvider] The data provider that produced the hash
85
+ # @param data_value [Object] The data value
86
+ # @return [Object] The data value
87
+ def validate_data_value(data_provider, value)
88
+ # The DataProvider.value_type is self recursive so further recursive check of collections is needed here
89
+ Types::TypeAsserter.assert_instance_of(nil, DataProvider.value_type, value) { "Value returned from #{data_provider.name}" }
99
90
  end
100
91
  end
101
92
  end
@@ -21,9 +21,12 @@ class FunctionProvider
21
21
 
22
22
  # @return [FunctionContext] the function context associated with this provider
23
23
  def function_context(lookup_invocation, location)
24
+ @contexts[location] ||= create_function_context(lookup_invocation)
25
+ end
26
+
27
+ def create_function_context(lookup_invocation)
24
28
  scope = lookup_invocation.scope
25
- compiler = scope.compiler
26
- @contexts[location] ||= FunctionContext.new(compiler.environment.name, module_name, function(scope))
29
+ FunctionContext.new(EnvironmentContext.adapt(scope.compiler.environment), module_name, function(scope))
27
30
  end
28
31
 
29
32
  def module_name
@@ -22,19 +22,24 @@ class GlobalDataProvider < ConfiguredDataProvider
22
22
  lookup_invocation.explainer)
23
23
  end
24
24
 
25
+ merge = MergeStrategy.strategy(merge)
25
26
  unless config.merge_strategy.is_a?(DefaultMergeStrategy)
26
- if lookup_invocation.hiera_xxx_call?
27
- # Merge strategy of the hiera_xxx call should only be applied when no merge strategy is defined in the hiera config
28
- merge = config.merge_strategy
29
- lookup_invocation.set_hiera_v3_merge_behavior
30
- elsif merge.is_a?(DefaultMergeStrategy)
31
- # For all other calls, the strategy of the call overrides the strategy defined in the hiera config
27
+ if lookup_invocation.hiera_xxx_call? && merge.is_a?(HashMergeStrategy)
28
+ # Merge strategy defined in the hiera config only applies when the call stems from a hiera_hash call.
32
29
  merge = config.merge_strategy
33
30
  lookup_invocation.set_hiera_v3_merge_behavior
34
31
  end
35
32
  end
33
+
34
+ value = super(key, lookup_invocation, merge)
35
+ if lookup_invocation.hiera_xxx_call? && (merge.is_a?(HashMergeStrategy) || merge.is_a?(DeepMergeStrategy))
36
+ # hiera_hash calls should error when found values are not hashes
37
+ Types::TypeAsserter.assert_instance_of('value', Types::PHashType::DEFAULT, value)
38
+ end
39
+ value
40
+ else
41
+ super
36
42
  end
37
- super(key, lookup_invocation, merge)
38
43
  end
39
44
 
40
45
  protected
@@ -8,15 +8,19 @@ module Lookup
8
8
 
9
9
  # @api private
10
10
  class ScopeLookupCollectingInvocation < Invocation
11
- attr_reader :scope_interpolations
12
-
13
11
  def initialize(scope)
14
12
  super(scope)
15
13
  @scope_interpolations = []
16
14
  end
17
15
 
18
- def remember_scope_lookup(*lookup_result)
19
- @scope_interpolations << lookup_result
16
+ def remember_scope_lookup(key, root_key, segments, value)
17
+ @scope_interpolations << [key, root_key, segments, value] unless !value.nil? && key.start_with?('::')
18
+ end
19
+
20
+ def scope_interpolations
21
+ # Save extra checks by keeping the array unique with respect to the key (first entry)
22
+ @scope_interpolations.uniq! { |si| si[0] }
23
+ @scope_interpolations
20
24
  end
21
25
  end
22
26
 
@@ -48,6 +52,7 @@ class HieraConfig
48
52
  KEY_V3_BACKEND = V3BackendFunctionProvider::TAG
49
53
  KEY_V4_DATA_HASH = V4DataHashFunctionProvider::TAG
50
54
  KEY_BACKEND = 'backend'.freeze
55
+ KEY_EXTENSION = 'extension'.freeze
51
56
 
52
57
  FUNCTION_KEYS = [KEY_DATA_HASH, KEY_LOOKUP_KEY, KEY_DATA_DIG, KEY_V3_BACKEND]
53
58
  ALL_FUNCTION_KEYS = FUNCTION_KEYS + [KEY_V4_DATA_HASH]
@@ -99,22 +104,28 @@ class HieraConfig
99
104
  # Creates a new HieraConfig from the given _config_root_. This is where the 'hiera.yaml' is expected to be found
100
105
  # and is also the base location used when resolving relative paths.
101
106
  #
107
+ # @param lookup_invocation [Invocation] Invocation data containing scope, overrides, and defaults
102
108
  # @param config_path [Pathname] Absolute path to the configuration file
103
109
  # @return [LookupConfiguration] the configuration
104
- def self.create(config_path)
110
+ def self.create(lookup_invocation, config_path)
105
111
  if config_path.is_a?(Hash)
106
112
  config_path = nil
107
113
  loaded_config = config_path
108
114
  else
109
115
  config_root = config_path.parent
110
116
  if config_path.exist?
111
- loaded_config = YAML.load_file(config_path)
112
-
113
- # For backward compatibility, we must treat an empty file, or a yaml that doesn't
114
- # produce a Hash as Hiera version 3 default.
115
- unless loaded_config.is_a?(Hash)
116
- Puppet.warning("#{config_path}: File exists but does not contain a valid YAML hash. Falling back to Hiera version 3 default config")
117
- loaded_config = HieraConfigV3::DEFAULT_CONFIG_HASH
117
+ env_context = EnvironmentContext.adapt(lookup_invocation.scope.compiler.environment)
118
+ loaded_config = env_context.cached_file_data(config_path) do |content|
119
+ parsed = YAML.load(content, config_path)
120
+
121
+ # For backward compatibility, we must treat an empty file, or a yaml that doesn't
122
+ # produce a Hash as Hiera version 3 default.
123
+ if parsed.is_a?(Hash)
124
+ parsed
125
+ else
126
+ Puppet.warning("#{config_path}: File exists but does not contain a valid YAML hash. Falling back to Hiera version 3 default config")
127
+ HieraConfigV3::DEFAULT_CONFIG_HASH
128
+ end
118
129
  end
119
130
  else
120
131
  config_path = nil
@@ -169,15 +180,21 @@ class HieraConfig
169
180
  end
170
181
 
171
182
  def scope_interpolations_stable?(lookup_invocation)
172
- scope = lookup_invocation.scope
173
- @scope_interpolations.all? do |key, root_key, segments, old_value|
174
- value = scope[root_key]
175
- unless value.nil? || segments.empty?
176
- found = '';
177
- catch(:no_such_key) { found = sub_lookup(key, lookup_invocation, segments, value) }
178
- value = found;
183
+ if @scope_interpolations.empty?
184
+ true
185
+ else
186
+ scope = lookup_invocation.scope
187
+ lookup_invocation.without_explain do
188
+ @scope_interpolations.all? do |key, root_key, segments, old_value|
189
+ value = scope[root_key]
190
+ unless value.nil? || segments.empty?
191
+ found = '';
192
+ catch(:no_such_key) { found = sub_lookup(key, lookup_invocation, segments, value) }
193
+ value = found;
194
+ end
195
+ old_value.eql?(value)
196
+ end
179
197
  end
180
- old_value.eql?(value)
181
198
  end
182
199
  end
183
200
 
@@ -297,9 +314,19 @@ class HieraConfigV3 < HieraConfig
297
314
  [@config[KEY_BACKENDS]].flatten.each do |backend|
298
315
  raise Puppet::DataBinding::LookupError, "#{@config_path}: Backend '#{backend}' defined more than once" if data_providers.include?(backend)
299
316
  original_paths = [@config[KEY_HIERARCHY]].flatten
300
- backend_config = @config[backend] || EMPTY_HASH
301
- datadir = Pathname(interpolate(backend_config[KEY_DATADIR] || default_datadir, lookup_invocation, false))
302
- ext = backend == 'hocon' ? '.conf' : ".#{backend}"
317
+ backend_config = @config[backend]
318
+ if backend_config.nil?
319
+ backend_config = EMPTY_HASH
320
+ else
321
+ backend_config = interpolate(backend_config, lookup_invocation, false)
322
+ end
323
+ datadir = Pathname(backend_config[KEY_DATADIR] || interpolate(default_datadir, lookup_invocation, false))
324
+ ext = backend_config[KEY_EXTENSION]
325
+ if ext.nil?
326
+ ext = backend == 'hocon' ? '.conf' : ".#{backend}"
327
+ else
328
+ ext = ".#{ext}"
329
+ end
303
330
  paths = resolve_paths(datadir, original_paths, lookup_invocation, @config_path.nil?, ext)
304
331
  data_providers[backend] = case
305
332
  when backend == 'json', backend == 'yaml'
@@ -510,6 +537,7 @@ class HieraConfigV5 < HieraConfig
510
537
  end
511
538
 
512
539
  entry_datadir = @config_root + (he[KEY_DATADIR] || datadir)
540
+ entry_datadir = Pathname(interpolate(entry_datadir.to_s, lookup_invocation, false))
513
541
  location_key = LOCATION_KEYS.find { |key| he.include?(key) }
514
542
  locations = case location_key
515
543
  when KEY_PATHS