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:
Tom Cammann 2015-10-22 13:40:16 +01:00 committed by Alexis Lee
parent 3dfd4a4e65
commit f6c668bfb3
6 changed files with 149 additions and 1 deletions

View File

@ -15,6 +15,7 @@ Option Definitions
.. autoclass:: MultiStrOpt
.. autoclass:: IPOpt
.. autoclass:: PortOpt
.. autoclass:: HostnameOpt
.. autoclass:: DeprecatedOpt
.. autoclass:: SubCommandOpt
.. autoclass:: OptGroup

View File

@ -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.

View File

@ -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()

View File

@ -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']])])],

View File

@ -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])

View File

@ -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