Skip to content

Commit 7cffaac

Browse files
Jenkinsopenstack-gerrit
authored andcommitted
Merge "Fixes "bad format" in replicator for valid hosts"
2 parents 001dd01 + ba1f41d commit 7cffaac

File tree

4 files changed

+230
-39
lines changed

4 files changed

+230
-39
lines changed

glance/cmd/replicator.py

Lines changed: 9 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
#!/usr/bin/env python
22

33
# Copyright 2012 Michael Still and Canonical Inc
4+
# Copyright 2014 SoftLayer Technologies, Inc.
45
# All Rights Reserved.
56
#
67
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -23,12 +24,12 @@
2324
import logging.handlers
2425
import optparse
2526
import os
26-
import re
2727
import sys
2828
import uuid
2929

3030
import six.moves.urllib.parse as urlparse
3131

32+
from glance.common import utils
3233
from glance.openstack.common import jsonutils
3334

3435
# If ../glance/__init__.py exists, add ../ to Python search path, so that
@@ -58,8 +59,6 @@
5859
'do not have permissions to see all '
5960
'the images on the slave server.')
6061

61-
SERVER_PORT_REGEX = '\w+:\w+'
62-
6362

6463
class AuthenticationException(Exception):
6564
pass
@@ -281,12 +280,7 @@ def replication_size(options, args):
281280
if len(args) < 1:
282281
raise TypeError(_("Too few arguments."))
283282

284-
server_port = args.pop()
285-
286-
if not re.match(SERVER_PORT_REGEX, server_port):
287-
raise ValueError(_("Bad format of the given arguments."))
288-
289-
server, port = server_port.split(':')
283+
server, port = utils.parse_valid_host_port(args.pop())
290284

291285
total_size = 0
292286
count = 0
@@ -319,12 +313,7 @@ def replication_dump(options, args):
319313
raise TypeError(_("Too few arguments."))
320314

321315
path = args.pop()
322-
server_port = args.pop()
323-
324-
if not re.match(SERVER_PORT_REGEX, server_port):
325-
raise ValueError(_("Bad format of the given arguments."))
326-
327-
server, port = server_port.split(':')
316+
server, port = utils.parse_valid_host_port(args.pop())
328317

329318
imageservice = get_image_service()
330319
client = imageservice(httplib.HTTPConnection(server, port),
@@ -404,12 +393,7 @@ def replication_load(options, args):
404393
raise TypeError(_("Too few arguments."))
405394

406395
path = args.pop()
407-
server_port = args.pop()
408-
409-
if not re.match(SERVER_PORT_REGEX, server_port):
410-
raise ValueError(_("Bad format of the given arguments."))
411-
412-
server, port = server_port.split(':')
396+
server, port = utils.parse_valid_host_port(args.pop())
413397

414398
imageservice = get_image_service()
415399
client = imageservice(httplib.HTTPConnection(server, port),
@@ -482,20 +466,13 @@ def replication_livecopy(options, args):
482466
if len(args) < 2:
483467
raise TypeError(_("Too few arguments."))
484468

485-
slave_server_port = args.pop()
486-
master_server_port = args.pop()
487-
488-
if not re.match(SERVER_PORT_REGEX, slave_server_port) or \
489-
not re.match(SERVER_PORT_REGEX, master_server_port):
490-
raise ValueError(_("Bad format of the given arguments."))
491-
492469
imageservice = get_image_service()
493470

494-
slave_server, slave_port = slave_server_port.split(':')
471+
slave_server, slave_port = utils.parse_valid_host_port(args.pop())
495472
slave_conn = httplib.HTTPConnection(slave_server, slave_port)
496473
slave_client = imageservice(slave_conn, options.slavetoken)
497474

498-
master_server, master_port = master_server_port.split(':')
475+
master_server, master_port = utils.parse_valid_host_port(args.pop())
499476
master_conn = httplib.HTTPConnection(master_server, master_port)
500477
master_client = imageservice(master_conn, options.mastertoken)
501478

@@ -559,20 +536,13 @@ def replication_compare(options, args):
559536
if len(args) < 2:
560537
raise TypeError(_("Too few arguments."))
561538

562-
slave_server_port = args.pop()
563-
master_server_port = args.pop()
564-
565-
if not re.match(SERVER_PORT_REGEX, slave_server_port) or \
566-
not re.match(SERVER_PORT_REGEX, master_server_port):
567-
raise ValueError(_("Bad format of the given arguments."))
568-
569539
imageservice = get_image_service()
570540

571-
slave_server, slave_port = slave_server_port.split(':')
541+
slave_server, slave_port = utils.parse_valid_host_port(args.pop())
572542
slave_conn = httplib.HTTPConnection(slave_server, slave_port)
573543
slave_client = imageservice(slave_conn, options.slavetoken)
574544

575-
master_server, master_port = master_server_port.split(':')
545+
master_server, master_port = utils.parse_valid_host_port(args.pop())
576546
master_conn = httplib.HTTPConnection(master_server, master_port)
577547
master_client = imageservice(master_conn, options.mastertoken)
578548

glance/common/utils.py

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
# Copyright 2010 United States Government as represented by the
22
# Administrator of the National Aeronautics and Space Administration.
3+
# Copyright 2014 SoftLayer Technologies, Inc.
34
# All Rights Reserved.
45
#
56
# Licensed under the Apache License, Version 2.0 (the "License"); you may
@@ -29,17 +30,20 @@
2930
import functools
3031
import os
3132
import platform
33+
import re
3234
import subprocess
3335
import sys
3436
import uuid
3537

38+
import netaddr
3639
from OpenSSL import crypto
3740
from oslo.config import cfg
3841
from webob import exc
3942

4043
from glance.common import exception
4144
from glance.openstack.common import excutils
4245
import glance.openstack.common.log as logging
46+
from glance.openstack.common import network_utils
4347
from glance.openstack.common import strutils
4448

4549
CONF = cfg.CONF
@@ -559,3 +563,73 @@ def is_uuid_like(val):
559563
return str(uuid.UUID(val)) == val
560564
except (TypeError, ValueError, AttributeError):
561565
return False
566+
567+
568+
def is_valid_port(port):
569+
"""Verify that port represents a valid port number."""
570+
return str(port).isdigit() and int(port) > 0 and int(port) <= 65535
571+
572+
573+
def is_valid_ipv4(address):
574+
"""Verify that address represents a valid IPv4 address."""
575+
try:
576+
return netaddr.valid_ipv4(address)
577+
except Exception:
578+
return False
579+
580+
581+
def is_valid_ipv6(address):
582+
"""Verify that address represents a valid IPv6 address."""
583+
try:
584+
return netaddr.valid_ipv6(address)
585+
except Exception:
586+
return False
587+
588+
589+
def is_valid_hostname(hostname):
590+
"""Verify whether a hostname (not an FQDN) is valid."""
591+
return re.match('^[a-zA-Z0-9-]+$', hostname) is not None
592+
593+
594+
def is_valid_fqdn(fqdn):
595+
"""Verify whether a host is a valid FQDN."""
596+
return re.match('^[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$', fqdn) is not None
597+
598+
599+
def parse_valid_host_port(host_port):
600+
"""
601+
Given a "host:port" string, attempts to parse it as intelligently as
602+
possible to determine if it is valid. This includes IPv6 [host]:port form,
603+
IPv4 ip:port form, and hostname:port or fqdn:port form.
604+
605+
Invalid inputs will raise a ValueError, while valid inputs will return
606+
a (host, port) tuple where the port will always be of type int.
607+
"""
608+
609+
try:
610+
try:
611+
host, port = network_utils.parse_host_port(host_port)
612+
except Exception:
613+
raise ValueError(_('Host and port "%s" is not valid.') % host_port)
614+
615+
if not is_valid_port(port):
616+
raise ValueError(_('Port "%s" is not valid.') % port)
617+
618+
# First check for valid IPv6 and IPv4 addresses, then a generic
619+
# hostname. Failing those, if the host includes a period, then this
620+
# should pass a very generic FQDN check. The FQDN check for letters at
621+
# the tail end will weed out any hilariously absurd IPv4 addresses.
622+
623+
if not (is_valid_ipv6(host) or is_valid_ipv4(host) or
624+
is_valid_hostname(host) or is_valid_fqdn(host)):
625+
raise ValueError(_('Host "%s" is not valid.') % host)
626+
627+
except Exception as ex:
628+
raise ValueError(_('%s '
629+
'Please specify a host:port pair, where host is an '
630+
'IPv4 address, IPv6 address, hostname, or FQDN. If '
631+
'using an IPv6 address, enclose it in brackets '
632+
'separately from the port (i.e., '
633+
'"[fe80::a:b:c]:9876").') % ex)
634+
635+
return (host, int(port))

glance/tests/unit/common/test_utils.py

Lines changed: 146 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -217,6 +217,152 @@ def test_validate_key_cert_key_cant_read(self):
217217
utils.validate_key_cert,
218218
keyf.name, keyf.name)
219219

220+
def test_valid_port(self):
221+
valid_inputs = [1, '1', 2, '3', '5', 8, 13, 21,
222+
'80', '3246', '65535']
223+
for input_str in valid_inputs:
224+
self.assertTrue(utils.is_valid_port(input_str))
225+
226+
def test_valid_port_fail(self):
227+
invalid_inputs = ['-32768', '0', 0, '65536', 528491, '528491',
228+
'528.491', 'thirty-seven']
229+
for input_str in invalid_inputs:
230+
self.assertFalse(utils.is_valid_port(input_str))
231+
232+
def test_valid_ipv4(self):
233+
valid_inputs = ['10.11.12.13',
234+
'172.17.17.1']
235+
for input_str in valid_inputs:
236+
self.assertTrue(utils.is_valid_ipv4(input_str))
237+
238+
def test_valid_ipv4_fail(self):
239+
invalid_pairs = ['',
240+
'290.12.52.80',
241+
'a.b.c.d',
242+
u'\u2601',
243+
u'\u2603:8080',
244+
'fe80::1',
245+
'[fe80::2]',
246+
'<fe80::3>:5673',
247+
'fe80:a:b:c:d:e:f:1:2:3:4',
248+
'fe80:a:b:c:d:e:f:g',
249+
'fe80::1:8080',
250+
'[fe80:a:b:c:d:e:f:g]:9090',
251+
'[a:b:s:u:r:d]:fe80']
252+
253+
for pair in invalid_pairs:
254+
self.assertRaises(ValueError,
255+
utils.parse_valid_host_port,
256+
pair)
257+
258+
def test_valid_ipv6(self):
259+
valid_inputs = ['fe80::1',
260+
'fe80:0000:0000:0000:0000:0000:0000:0002',
261+
'fe80:a:b:c:d:e:f:0',
262+
'fe80::a:b:c:d',
263+
'fe80::1:8080']
264+
265+
for input_str in valid_inputs:
266+
self.assertTrue(utils.is_valid_ipv6(input_str))
267+
268+
def test_valid_ipv6_fail(self):
269+
invalid_pairs = ['',
270+
'[fe80::2]',
271+
'<fe80::3>',
272+
'fe80:::a',
273+
'fe80:a:b:c:d:e:f:1:2:3:4',
274+
'fe80:a:b:c:d:e:f:g',
275+
'fe80::1:8080',
276+
'i:n:s:a:n:i:t:y']
277+
278+
for pair in invalid_pairs:
279+
self.assertRaises(ValueError,
280+
utils.parse_valid_host_port,
281+
pair)
282+
283+
def test_valid_hostname(self):
284+
valid_inputs = ['localhost',
285+
'glance04-a'
286+
'G',
287+
'528491']
288+
289+
for input_str in valid_inputs:
290+
self.assertTrue(utils.is_valid_hostname(input_str))
291+
292+
def test_valid_hostname_fail(self):
293+
invalid_inputs = ['localhost.localdomain',
294+
'192.168.0.1',
295+
u'\u2603',
296+
'glance02.stack42.local']
297+
298+
for input_str in invalid_inputs:
299+
self.assertFalse(utils.is_valid_hostname(input_str))
300+
301+
def test_valid_fqdn(self):
302+
valid_inputs = ['localhost.localdomain',
303+
'glance02.stack42.local'
304+
'glance04-a.stack47.local',
305+
'img83.glance.xn--penstack-r74e.org']
306+
307+
for input_str in valid_inputs:
308+
self.assertTrue(utils.is_valid_fqdn(input_str))
309+
310+
def test_valid_fqdn_fail(self):
311+
invalid_inputs = ['localhost',
312+
'192.168.0.1',
313+
'999.88.77.6',
314+
u'\u2603.local',
315+
'glance02.stack42']
316+
317+
for input_str in invalid_inputs:
318+
self.assertFalse(utils.is_valid_fqdn(input_str))
319+
320+
def test_valid_host_port_string(self):
321+
valid_pairs = ['10.11.12.13:80',
322+
'172.17.17.1:65535',
323+
'[fe80::a:b:c:d]:9990',
324+
'localhost:9990',
325+
'localhost.localdomain:9990',
326+
'glance02.stack42.local:1234',
327+
'glance04-a.stack47.local:1234',
328+
'img83.glance.xn--penstack-r74e.org:13080']
329+
330+
for pair_str in valid_pairs:
331+
host, port = utils.parse_valid_host_port(pair_str)
332+
333+
escaped = pair_str.startswith('[')
334+
expected_host = '%s%s%s' % ('[' if escaped else '', host,
335+
']' if escaped else '')
336+
337+
self.assertTrue(pair_str.startswith(expected_host))
338+
self.assertTrue(port > 0)
339+
340+
expected_pair = '%s:%d' % (expected_host, port)
341+
self.assertEqual(expected_pair, pair_str)
342+
343+
def test_valid_host_port_string_fail(self):
344+
invalid_pairs = ['',
345+
'10.11.12.13',
346+
'172.17.17.1:99999',
347+
'290.12.52.80:5673',
348+
'absurd inputs happen',
349+
u'\u2601',
350+
u'\u2603:8080',
351+
'fe80::1',
352+
'[fe80::2]',
353+
'<fe80::3>:5673',
354+
'[fe80::a:b:c:d]9990',
355+
'fe80:a:b:c:d:e:f:1:2:3:4',
356+
'fe80:a:b:c:d:e:f:g',
357+
'fe80::1:8080',
358+
'[fe80:a:b:c:d:e:f:g]:9090',
359+
'[a:b:s:u:r:d]:fe80']
360+
361+
for pair in invalid_pairs:
362+
self.assertRaises(ValueError,
363+
utils.parse_valid_host_port,
364+
pair)
365+
220366

221367
class UUIDTestCase(test_utils.BaseTestCase):
222368

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ iso8601>=0.1.9
2222
ordereddict
2323
oslo.config>=1.2.0
2424
stevedore>=0.14
25+
netaddr>=0.7.6
2526

2627
# For Swift storage backend.
2728
python-swiftclient>=1.6

0 commit comments

Comments
 (0)