Repository: ansible/ansible

Issue:
Fix a parsing bug that prevents v6 addresses from being used with add_host



Discussion:

Resolving patch:
diff --git a/lib/ansible/plugins/action/add_host.py b/lib/ansible/plugins/action/add_host.py
--- a/lib/ansible/plugins/action/add_host.py
+++ b/lib/ansible/plugins/action/add_host.py
@@ -20,6 +20,8 @@
 from __future__ import (absolute_import, division, print_function)
 __metaclass__ = type
 
+import re
+
 from ansible.plugins.action import ActionBase
 
 class ActionModule(ActionBase):
@@ -38,8 +40,8 @@ def run(self, tmp=None, task_vars=dict()):
         new_name = self._task.args.get('name', self._task.args.get('hostname', None))
         #vv("creating host via 'add_host': hostname=%s" % new_name)
 
-        if ":" in new_name:
-            new_name, new_port = new_name.split(":")
+        new_name, new_port = _parse_ip_host_and_port(new_name)
+        if new_port:
             self._task.args['ansible_ssh_port'] = new_port
 
         groups = self._task.args.get('groupname', self._task.args.get('groups', self._task.args.get('group', ''))) 
@@ -58,4 +60,26 @@ def run(self, tmp=None, task_vars=dict()):
 
         return dict(changed=True, add_host=dict(host_name=new_name, groups=new_groups, host_vars=host_vars))
 
+def _parse_ip_host_and_port(hostname):
+    """
+    Attempt to parse the hostname and port from a hostname, e.g.,
 
+    some-host-name
+    some-host-name:80
+    8.8.8.8
+    8.8.8.8:80
+    2001:db8:0:1
+    [2001:db8:0:1]:80
+    """
+    if hostname.count(':') > 1:
+        match = re.match(
+            '\[(?P<ip>[^\]]+)\](:(?P<port>[0-9]+))?',
+            hostname
+        )
+        if match:
+            return match.group('ip'), match.group('port')
+        else:
+            return hostname, None
+    elif ':' in hostname:
+        return hostname.rsplit(':', 1)
+    return hostname, None


Test patch:
diff --git a/test/units/plugins/action/test_add_host.py b/test/units/plugins/action/test_add_host.py
new file mode 100644
--- /dev/null
+++ b/test/units/plugins/action/test_add_host.py
@@ -0,0 +1,47 @@
+import unittest
+
+from ansible.plugins.action import add_host
+
+
+class TestAddHost(unittest.TestCase):
+
+    def test_hostname(self):
+        host, port = add_host._parse_ip_host_and_port('some-remote-host')
+        assert host == 'some-remote-host'
+        assert port is None
+
+    def test_hostname_with_port(self):
+        host, port = add_host._parse_ip_host_and_port('some-remote-host:80')
+        assert host == 'some-remote-host'
+        assert port == '80'
+
+    def test_parse_ip_host_and_port_v4(self):
+        host, port = add_host._parse_ip_host_and_port('8.8.8.8')
+        assert host == '8.8.8.8'
+        assert port is None
+
+    def test_parse_ip_host_and_port_v4_and_port(self):
+        host, port = add_host._parse_ip_host_and_port('8.8.8.8:80')
+        assert host == '8.8.8.8'
+        assert port == '80'
+
+    def test_parse_ip_host_and_port_v6(self):
+        host, port = add_host._parse_ip_host_and_port(
+            'dead:beef:dead:beef:dead:beef:dead:beef'
+        )
+        assert host == 'dead:beef:dead:beef:dead:beef:dead:beef'
+        assert port is None
+
+    def test_parse_ip_host_and_port_v6_with_brackets(self):
+        host, port = add_host._parse_ip_host_and_port(
+            '[dead:beef:dead:beef:dead:beef:dead:beef]'
+        )
+        assert host == 'dead:beef:dead:beef:dead:beef:dead:beef'
+        assert port is None
+
+    def test_parse_ip_host_and_port_v6_with_brackets_and_port(self):
+        host, port = add_host._parse_ip_host_and_port(
+            '[dead:beef:dead:beef:dead:beef:dead:beef]:80'
+        )
+        assert host == 'dead:beef:dead:beef:dead:beef:dead:beef'
+        assert port == '80'