Add hostname config type
Hostnames are often used in config files and they have specific validation requirements. This change adds a config to validate a correct hostname are specified in a config file. A hostname refers to a valid DNS or hostname. It must not be longer than 253 characters, have a segment greater than 63 characters and start or end with a hyphen. Closes-Bug: #1508943 Co-Authored-By: ChangBo Guo(gcb) <eric.guo@easystack.cn> Change-Id: Ic1028a437aee6076c4b7f437f60bbd209f38a20e
This commit is contained in:
parent
3dfd4a4e65
commit
f6c668bfb3
@ -15,6 +15,7 @@ Option Definitions
|
||||
.. autoclass:: MultiStrOpt
|
||||
.. autoclass:: IPOpt
|
||||
.. autoclass:: PortOpt
|
||||
.. autoclass:: HostnameOpt
|
||||
.. autoclass:: DeprecatedOpt
|
||||
.. autoclass:: SubCommandOpt
|
||||
.. autoclass:: OptGroup
|
||||
|
@ -58,6 +58,7 @@ Type Option
|
||||
:class:`oslo_config.types.List` :class:`oslo_config.cfg.ListOpt`
|
||||
:class:`oslo_config.types.Dict` :class:`oslo_config.cfg.DictOpt`
|
||||
:class:`oslo_config.types.IPAddress` :class:`oslo_config.cfg.IPOpt`
|
||||
:class:`oslo_config.types.Hostname` :class:`oslo_config.cfg.HostnameOpt`
|
||||
==================================== ======
|
||||
|
||||
For :class:`oslo_config.cfg.MultiOpt` the `item_type` parameter defines
|
||||
@ -1251,6 +1252,20 @@ class PortOpt(Opt):
|
||||
super(PortOpt, self).__init__(name, type=type, **kwargs)
|
||||
|
||||
|
||||
class HostnameOpt(Opt):
|
||||
|
||||
"""Option for a hostname. Only accepts valid hostnames.
|
||||
|
||||
Option with ``type`` :class:`oslo_config.types.Hostname`
|
||||
|
||||
.. versionadded:: 3.8
|
||||
"""
|
||||
|
||||
def __init__(self, name, **kwargs):
|
||||
super(HostnameOpt, self).__init__(name, type=types.Hostname(),
|
||||
**kwargs)
|
||||
|
||||
|
||||
class MultiOpt(Opt):
|
||||
|
||||
"""Multi-value option.
|
||||
|
@ -77,7 +77,8 @@ def _format_defaults(opt):
|
||||
elif opt.default is None:
|
||||
default_str = '<None>'
|
||||
elif (isinstance(opt, cfg.StrOpt) or
|
||||
isinstance(opt, cfg.IPOpt)):
|
||||
isinstance(opt, cfg.IPOpt) or
|
||||
isinstance(opt, cfg.HostnameOpt)):
|
||||
default_str = opt.default
|
||||
elif isinstance(opt, cfg.BoolOpt):
|
||||
default_str = str(opt.default).lower()
|
||||
|
@ -142,6 +142,9 @@ class GeneratorTestCase(base.BaseTestCase):
|
||||
'port_opt': cfg.PortOpt('port_opt',
|
||||
default=80,
|
||||
help='a port'),
|
||||
'hostname_opt': cfg.HostnameOpt('hostname_opt',
|
||||
default='compute01.nova.site1',
|
||||
help='a hostname'),
|
||||
'multi_opt': cfg.MultiStrOpt('multi_opt',
|
||||
default=['1', '2', '3'],
|
||||
help='multiple strings'),
|
||||
@ -604,6 +607,17 @@ class GeneratorTestCase(base.BaseTestCase):
|
||||
# Minimum value: 0
|
||||
# Maximum value: 65535
|
||||
#port_opt = 80
|
||||
''')),
|
||||
('hostname_opt',
|
||||
dict(opts=[('test', [(None, [opts['hostname_opt']])])],
|
||||
expected='''[DEFAULT]
|
||||
|
||||
#
|
||||
# From test
|
||||
#
|
||||
|
||||
# a hostname (hostname value)
|
||||
#hostname_opt = compute01.nova.site1
|
||||
''')),
|
||||
('multi_opt',
|
||||
dict(opts=[('test', [(None, [opts['multi_opt']])])],
|
||||
|
@ -551,3 +551,71 @@ class IPv6AddressTypeTests(IPAddressTypeTests):
|
||||
|
||||
def test_ipv4_address(self):
|
||||
self.assertInvalid('192.168.0.1')
|
||||
|
||||
|
||||
class HostnameTypeTests(TypeTestHelper, unittest.TestCase):
|
||||
type = types.Hostname()
|
||||
|
||||
def assertConvertedEqual(self, value):
|
||||
self.assertConvertedValue(value, value)
|
||||
|
||||
def test_empty_hostname_fails(self):
|
||||
self.assertInvalid('')
|
||||
|
||||
def test_should_return_same_hostname_if_valid(self):
|
||||
self.assertConvertedEqual('foo.bar')
|
||||
|
||||
def test_trailing_quote_is_invalid(self):
|
||||
self.assertInvalid('foo.bar"')
|
||||
|
||||
def test_repr(self):
|
||||
self.assertEqual('Hostname', repr(types.Hostname()))
|
||||
|
||||
def test_equal(self):
|
||||
self.assertEqual(types.Hostname(), types.Hostname())
|
||||
self.assertEqual(types.Hostname(), types.Hostname())
|
||||
|
||||
def test_not_equal_to_other_class(self):
|
||||
self.assertNotEqual(types.Hostname(), types.Integer())
|
||||
self.assertNotEqual(types.Hostname(), types.String())
|
||||
|
||||
def test_invalid_characters(self):
|
||||
self.assertInvalid('"host"')
|
||||
self.assertInvalid("h'ost'")
|
||||
self.assertInvalid("h'ost")
|
||||
self.assertInvalid("h$ost")
|
||||
self.assertInvalid("h%ost")
|
||||
self.assertInvalid("host_01.co.uk")
|
||||
self.assertInvalid("host;name=99")
|
||||
self.assertInvalid('___site0.1001')
|
||||
self.assertInvalid('_site01001')
|
||||
self.assertInvalid("host..name")
|
||||
self.assertInvalid(".host.name.com")
|
||||
self.assertInvalid("no spaces")
|
||||
|
||||
def test_no_start_end_hyphens(self):
|
||||
self.assertInvalid("-host.com")
|
||||
self.assertInvalid("-hostname.com-")
|
||||
self.assertInvalid("hostname.co.uk-")
|
||||
|
||||
def test_strip_trailing_dot(self):
|
||||
self.assertConvertedValue('cell1.nova.site1.', 'cell1.nova.site1')
|
||||
self.assertConvertedValue('cell1.', 'cell1')
|
||||
|
||||
def test_valid_hostname(self):
|
||||
self.assertConvertedEqual('cell1.nova.site1')
|
||||
self.assertConvertedEqual('site01001')
|
||||
self.assertConvertedEqual('home-site-here.org.com')
|
||||
self.assertConvertedEqual('192.168.0.1')
|
||||
self.assertConvertedEqual('1.1.1')
|
||||
self.assertConvertedEqual('localhost')
|
||||
|
||||
def test_max_segment_size(self):
|
||||
self.assertConvertedEqual('host.%s.com' % ('x' * 63))
|
||||
self.assertInvalid('host.%s.com' % ('x' * 64))
|
||||
|
||||
def test_max_hostname_size(self):
|
||||
test_str = '.'.join('x'*31 for x in range(8))
|
||||
self.assertEqual(255, len(test_str))
|
||||
self.assertInvalid(test_str)
|
||||
self.assertConvertedEqual(test_str[:-2])
|
||||
|
@ -603,3 +603,52 @@ class IPAddress(ConfigType):
|
||||
|
||||
def _formatter(self, value):
|
||||
return value
|
||||
|
||||
|
||||
class Hostname(ConfigType):
|
||||
"""Hostname type.
|
||||
|
||||
A hostname refers to a valid DNS or hostname. It must not be longer than
|
||||
253 characters, have a segment greater than 63 characters, nor start or
|
||||
end with a hyphen.
|
||||
|
||||
:param type_name: Type name to be used in the sample config file.
|
||||
|
||||
"""
|
||||
|
||||
def __init__(self, type_name='hostname value'):
|
||||
super(Hostname, self).__init__(type_name=type_name)
|
||||
|
||||
def __call__(self, value):
|
||||
"""Check hostname is valid.
|
||||
|
||||
Ensures that each segment
|
||||
- Contains at least one character and a maximum of 63 characters
|
||||
- Consists only of allowed characters: letters (A-Z and a-z),
|
||||
digits (0-9), and hyphen (-)
|
||||
- Does not begin or end with a hyphen
|
||||
- maximum total length of 253 characters
|
||||
|
||||
For more details , please see: http://tools.ietf.org/html/rfc1035
|
||||
"""
|
||||
|
||||
if len(value) == 0:
|
||||
raise ValueError("Cannot have an empty hostname")
|
||||
if len(value) > 253:
|
||||
raise ValueError("hostname is greater than 253 characters: %s"
|
||||
% value)
|
||||
if value.endswith("."):
|
||||
value = value[:-1]
|
||||
allowed = re.compile("(?!-)[A-Z0-9-]{1,63}(?<!-)$", re.IGNORECASE)
|
||||
if any((not allowed.match(x)) for x in value.split(".")):
|
||||
raise ValueError("%s is an invalid hostname" % value)
|
||||
return value
|
||||
|
||||
def __repr__(self):
|
||||
return 'Hostname'
|
||||
|
||||
def __eq__(self, other):
|
||||
return self.__class__ == other.__class__
|
||||
|
||||
def _formatter(self, value):
|
||||
return value
|
||||
|
Loading…
Reference in New Issue
Block a user