{"action":"created","comment":{"url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments/64179634","id":64179634,"diff_hunk":"@@ -0,0 +1,615 @@\n+#!/usr/bin/python\n+#\n+# This module is free software: you can redistribute it and/or modify\n+# it under the terms of the GNU General Public License as published by\n+# the Free Software Foundation, either version 3 of the License, or\n+# (at your option) any later version.\n+#\n+# This software is distributed in the hope that it will be useful,\n+# but WITHOUT ANY WARRANTY; without even the implied warranty of\n+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the\n+# GNU General Public License for more details.\n+#\n+# You should have received a copy of the GNU General Public License\n+# along with this software.  If not, see <http://www.gnu.org/licenses/>.\n+\n+DOCUMENTATION = '''\n+---\n+module: ip\n+version_added: 2.2\n+author: \"Felix Engelmann (@felix-engelmann)\"\n+short_description: Wrapper of the linux ip tool\n+requirements: [ pyroute2 ]\n+description:\n+    - Performs linux ip address manipulations for interfaces and route adjustments.\n+options:\n+    mode:\n+        required: true\n+        choices: [ address, route, link ]\n+        description:\n+            - Whether to change an address, route or interface.\n+    state:\n+        required: false\n+        default: \"present\"\n+        choices: [ present, absent ]\n+        description:\n+            - Whether the address should be assigned to the interface or removed\n+              respectively whether the route should be set or removed.\n+              or the interface should be brought up or down\n+    addr:\n+        required: false\n+        aliases: [ net ]\n+        description:\n+            - The address to set or remove with prefix length. (e.g. 2001:db8::2/64, 10.0.0.2/24, 10.0.0.2/255.255.255.0)\n+              For nets, the default route is specified by v4default or v6default .\n+    dev:\n+        required: true\n+        description:\n+            - The device to set or remove the address/route from. Or the name of the new link.\n+    via:\n+        required: false\n+        description:\n+            - IP address of gateway for manipulating routes.\n+    kind:\n+        required: false\n+        choices: [ bridge, vlan, veth, port ]\n+        description:\n+            - Type of link to create or to set/remove interface as port of bridge.\n+    link:\n+        required: false\n+        aliases: [ peer, master ]\n+        description:\n+            - The interface name of the parent link for vlan or second peer for veth interfaces.\n+    vlan_id:\n+        required: false\n+        description:\n+            - The vlan ID for vlan interfaces.\n+'''\n+\n+EXAMPLES = '''\n+# set address 2001:db8::42/64 to interface eth0\n+- ip: mode=address addr=2001:db8::42/64 dev=eth0 state=present\n+\n+# change prefix length (watch out that any other assignments of same address with different prefix will be deleted)\n+- ip: mode=address addr=2001:db8::42/112 dev=eth0 state=present\n+\n+# remove address from interface eth0\n+- ip: mode=address addr=2001:db8::42/112 dev=eth0 state=absent\n+\n+# add route for net 2001:db3::/48 via fe80::3123 through interface eth0\n+- ip: mode=route net=2001:db3::/48 via=fe80::3123 dev=eth0\n+\n+# add IPv4 default route through gateway 192.168.0.1 at eth0\n+- ip: mode=route net=v4default via=192.168.0.1 dev=eth0\n+\n+# remove IPv6 default route at interface eth0\n+- ip: mode=route net=::/0 dev=eth0 state=absent\n+\n+# create bridge br0\n+- ip: mode=link dev=br0 kind=bridge\n+\n+# add eth0 to bridge br0\n+- ip: mode=link dev=eth0 kind=port master=br0\n+\n+# remove eth0 from bridge\n+- ip: mode=link dev=eth0 kind=port state=absent\n+\n+# create a veth interface pair of v1p0 and v1p1\n+- ip: mode=link dev=v1p0 kind=veth peer=v1p1\n+\n+# add vlan interface with id 42 onto eth0\n+- ip: mode=link dev=v100 kind=vlan link=eth0 vlan_id=42\n+\n+# delete interface br0\n+- ip: mode=link dev=br0 state=absent \n+\n+# bring up existing interface\n+- ip: mode=link dev=eth1 \n+\n+# bring down pyhsical interface\n+- ip: mode=link dev=eth1 state=absent \n+\n+'''\n+\n+RETURN = '''\n+---\n+'''\n+\n+from ansible.module_utils.pycompat24 import get_exception\n+\n+try:\n+    from pyroute2 import IPRoute\n+    from pyroute2.netlink.exceptions import NetlinkError\n+    from pyroute2.netlink import AF_INET6, AF_INET\n+    import ipaddress\n+    HAS_PYROUTE=1\n+except ImportError:\n+    HAS_PYROUTE=0\n+\n+ip=None\n+module=None\n+\n+def l_key(l,key):\n+    elems=list(filter(lambda x: x[0] == key,l))\n+    if len(elems) == 0:\n+        return None\n+    else:\n+        return elems[0][1]\n+\n+def parse_ip(text):\n+    addr=None\n+    if text:\n+        if text == 'v4default':\n+            addr=ipaddress.IPv4Interface(u'0.0.0.0/0')\n+        elif text == 'v6default':\n+            addr=ipaddress.IPv6Interface(u'::/0')\n+        else:\n+            try:\n+                # is it an IPv6\n+                addr = ipaddress.IPv6Interface(u''+text)\n+            except ipaddress.AddressValueError:\n+                try:\n+                    # fall back to parse it as IPv4\n+                    addr = ipaddress.IPv4Interface(u''+text)\n+                except Exception:\n+                    e = get_exception()\n+                    module.fail_json(msg='can not parse ip %s : %s'%(text,str(e)))\n+            except Exception:\n+                e = get_exception()\n+                module.fail_json(msg='can not parse ip %s: %s'%(text,str(e)))\n+    return(addr)\n+\n+#object to hold all important information of a device\n+class Device(object):\n+    \n+    def __init__(self,name,devid,addresses,state,master,kind,vlanid,link):\n+        self.name=name\n+        self.id=devid\n+        self.addresses=addresses\n+        self.state=state\n+        self.master=master\n+        self.kind=kind\n+        self.vlanid=vlanid\n+        self.link=link\n+        \n+    def is_bridge(self):\n+        return self.kind == 'bridge'\n+        \n+    def is_vlan(self,vlan_id,link):\n+        return ( self.kind == 'vlan' and self.vlanid == vlan_id and self.link == link.id )\n+        \n+    def is_veth(self):\n+        return self.kind == 'veth'\n+    \n+    def is_up(self):\n+        return self.state == 'UP'\n+    \n+    # check if addr is assigned to this IF. Prefix has to match.\n+    def has_address(self, addr):\n+        return addr in self.addresses\n+    \n+    # check if ip is assiged. Prefixes can differ.\n+    def get_addr_diff_lens(self, addr):\n+        return [x for x in self.addresses if x.ip == addr.ip and x.network != addr.network]\n+        \n+    # adds address to interface (not reflected in local data)\n+    def add_addr(self, addr):\n+        try:\n+            ip.addr('add', index=self.id, address=addr.ip.compressed, prefixlen=addr.network.prefixlen)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not add address %s to device %s: %s'%(addr.ip.compressed,self.name,str(e)) )\n+    \n+    # deletes address from interface (not reflected in local data)\n+    def del_addr(self, addr):\n+        try:\n+            ip.addr('delete', index=self.id, address=addr.ip.compressed, prefixlen=addr.network.prefixlen)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not delete address %s from device %s: %s'%(addr.ip.compressed,self.name,str(e)) )\n+    \n+    \n+    def set_master(self,master):\n+        try:\n+            ip.link(\"set\", index=self.id, master=master.id)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not add device %s to bridge %s: %s'%(self.name,master.name,str(e)) )\n+            \n+    def set_up(self):\n+        try:\n+            ip.link(\"set\", index=self.id, state=\"up\")\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not up device %s: %s'%(self.name,str(e)) )\n+    \n+    def del_master(self):\n+        try:\n+            ip.link(\"set\", index=self.id, master=0)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not remove device %s from bridge: %s'%(self.name,str(e)) )\n+    \n+    def delete(self):\n+        try:\n+            if self.kind:\n+                ip.link(\"del\",index=self.id)\n+            else:\n+                # system interface\n+                ip.link(\"set\", index=self.id, state=\"down\")      \n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not delete or down device %s: %s'%(self.name,str(e)) )\n+    \n+    # factory device from device name\n+    @staticmethod\n+    def factoryDeviceFromName(ifname):\n+        devids = ip.link_lookup(ifname=ifname)\n+        if len(devids) != 1:\n+            return None\n+        else:\n+            # fetch address related information\n+            devaddrs = ip.get_addr(index=devids[0])\n+            \n+            addrs=[]\n+            \n+            for addr in devaddrs:\n+                ifip = l_key(addr['attrs'],'IFA_ADDRESS')\n+                prefixlen = addr['prefixlen']\n+                \n+                addrobj = parse_ip(ifip+\"/\"+str(prefixlen))\n+                if addrobj:\n+                    addrs.append(addrobj)\n+        \n+            # fetch link related information\n+            link = ip.get_links(devids[0])[0]['attrs']\n+            \n+            linkstate  = l_key(link,'IFLA_OPERSTATE')\n+            linkmaster = l_key(link,'IFLA_MASTER')\n+            linklink   = l_key(link,'IFLA_LINK')\n+            \n+            linkkind=None\n+            linkinfo = l_key(link,'IFLA_LINKINFO')\n+            vlanid = None\n+            if linkinfo:\n+                linkkind=l_key(linkinfo['attrs'],'IFLA_INFO_KIND')\n+                if linkkind == 'vlan':\n+                    infodata = l_key(linkinfo['attrs'],'IFLA_INFO_DATA')['attrs']\n+                    vlanid = l_key(infodata,'IFLA_VLAN_ID')\n+            \n+             \n+            return Device(ifname,devids[0],addrs,linkstate,linkmaster,linkkind,vlanid,linklink)\n+            \n+    # factory to create new bridge\n+    @staticmethod\n+    def factoryCreateBridge(ifname):\n+        try:\n+            ip.link(\"add\",ifname=ifname,kind=\"bridge\")\n+        except Exception:\n+            e = get_exception()\n+            module.fail_json(msg='could not create bridge %s: %s'%(ifname,str(e)))\n+        \n+        return Device.factoryDeviceFromName(ifname)\n+        \n+    # factory to create new vLan\n+    @staticmethod\n+    def factoryCreateVLAN(ifname,vlan_id,link):\n+        try:\n+            ip.link(\"add\",ifname=ifname,kind=\"vlan\",vlan_id=vlan_id,link=link.id)\n+        except Exception:\n+            e = get_exception()\n+            module.fail_json(msg='could not create vlan %s tag %d on device %s: %s'%(ifname,vlan_id,link.name,str(e)))\n+        \n+        return Device.factoryDeviceFromName(ifname)\n+    \n+    @staticmethod\n+    def factoryCreateVeth(ifname,peer):\n+        try:\n+            if peer:\n+                ip.link(\"add\",ifname=ifname,kind=\"veth\",peer=peer)\n+            else:\n+                ip.link(\"add\",ifname=ifname,kind=\"veth\")\n+        except Exception:\n+            e = get_exception()\n+            module.fail_json(msg='could not create veth %s peer %d: %s'%(ifname,peer,str(e)))\n+        \n+        return Device.factoryDeviceFromName(ifname)\n+    \n+    def dump(self):\n+        print(\"if: %s (%d) %s\"%(self.name,self.id,self.state))\n+        for a in self.addresses:\n+            print(\" - %s/%d\"%(a.ip.compressed,a.network.prefixlen))\n+\n+#object to hold all important information on a route\n+class Route(object):\n+    def __init__(self,net,interface,gateway=None):\n+        self.net=net\n+        self.interface=interface\n+        self.gateway=gateway\n+        \n+    def __eq__(self, other):\n+        if type(other) is type(self):\n+            return self.net == other.net and self.interface == other.interface\n+        return False\n+        \n+    def dump(self):\n+        if self.gateway:\n+            print(\"%s/%d -> %d (%s)\"%(self.net.ip.compressed,self.net.network.prefixlen\n+                                    ,self.interface,self.gateway.ip.compressed))\n+        else:\n+            print(\"%s/%d -> %d\"%(self.net.ip.compressed,self.net.network.prefixlen\n+                                    ,self.interface))\n+        \n+\n+#object to hold all important information for routes\n+class Routes(object):\n+    \n+    def __init__(self,routes):\n+        self.routes=routes\n+    \n+    def has_route(self,dst,dev,via=None):\n+        for r in [x for x in self.routes if x == Route(dst,dev.id)]:\n+            if r.gateway == via:\n+                return True\n+        return False\n+    \n+    def has_route_any_gw(self,dst,dev):\n+        return Route(dst,dev.id) in self.routes\n+        \n+    def del_route(self,dst,dev):\n+        try:\n+            ip.route('delete',dst=str(dst.network),oif=dev.id)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not delete route %s via device %s: %s'%(str(dst.network),dev.name,str(e)) )\n+        \n+    def add_route(self,dst,dev,via):\n+        try:\n+            if via:\n+                ip.route('add',dst=str(dst.network),oif=dev.id,gateway=str(via.ip))\n+            else:\n+                ip.route('add',dst=str(dst.network),oif=dev.id)\n+        except NetlinkError:\n+            e = get_exception()\n+            module.fail_json(msg='could not add route %s via device %s and gw %s: %s'%(str(dst.network)\n+                                ,dev.name,str(via),str(e)) )\n+    \n+    #factory get all Routes\n+    @staticmethod\n+    def factoryRoutes():\n+        rs=[]\n+        for family in [AF_INET,AF_INET6]:\n+            routes=ip.get_routes(family=family)\n+            for route in routes:\n+                rif   = l_key(route['attrs'],'RTA_OIF')\n+                rgw   = l_key(route['attrs'],'RTA_GATEWAY')\n+                rvia = None\n+                if rgw:\n+                    rvia = parse_ip(rgw)\n+                rdst  = l_key(route['attrs'],'RTA_DST')\n+                rplen = route['dst_len']\n+                if rplen == 0:\n+                    # default route\n+                    if family == AF_INET:\n+                        rdnet = parse_ip(\"v4default\")\n+                    else:\n+                        rdnet = parse_ip(\"v6default\")\n+                else:\n+                    rdnet = parse_ip(rdst+\"/\"+str(rplen))\n+                rs.append(Route(rdnet,rif,rvia))\n+        return Routes(rs)\n+    \n+    def dump(self):\n+        for r in self.routes:\n+            r.dump()\n+    \n+def main():\n+    # new Ansible module\n+    global module\n+    module = AnsibleModule(\n+        argument_spec = dict(\n+            mode  = dict(choices=['address','route','link'], default=None, required=True),\n+            state = dict(choices=['present','absent'], default='present'),\n+            addr  = dict(default=None,aliases=['net']),\n+            dev   = dict(default=None,required=True),\n+            via   = dict(default=None),\n+            kind  = dict(choices=['bridge','vlan','veth','port'],default=None),\n+            link  = dict(default=None,aliases=['peer','master']),\n+            vlan_id= dict(default=None,type='int'),\n+        ),\n+    )\n+    \n+    # this module can not work without pyroute2 and needs ipaddress for IP manipulations\n+    # ipaddress is shipped with python\n+    \n+    if not HAS_PYROUTE:\n+        module.fail_json(msg='pyroute2 is not installed')\n+    \n+    global ip    \n+    ip = IPRoute()\n+    \n+    # parameters and their processing\n+    params    = module.params\n+    mode      = params['mode']\n+    state     = params['state']\n+    addr      = parse_ip(params['addr'])\n+    dev       = Device.factoryDeviceFromName(params['dev'])\n+    dev_name  = params['dev']\n+    via       = parse_ip(params['via'])\n+    kind      = params['kind']\n+    link      = Device.factoryDeviceFromName(params['link'])\n+    link_name = params['link']\n+    vlan_id   = params['vlan_id']\n+     \n+    # separate different modes\n+    if mode == 'address':\n+        # check for required arguments\n+        if not (dev and addr):    \n+            module.fail_json(msg='valid device and address required')\n+        \n+        if dev.has_address(addr):\n+            if state == \"present\":\n+                module.exit_json(changed=False)\n+            else:\n+                dev.del_addr(addr)\n+                module.exit_json(changed=True)\n+        else:\n+            if state == \"present\":\n+                dev.add_addr(addr)\n+                # clean up possible equal addresses with different prefix length\n+                for badaddr in dev.get_addr_diff_lens(addr):\n+                    dev.del_addr(badaddr)\n+                \n+                module.exit_json(changed=True)\n+            else:\n+                changed=False\n+                # delete all prefixes with of this ip\n+                for badaddr in dev.get_addr_diff_lens(addr):\n+                    dev.del_addr(badaddr)\n+                    changed=True\n+                \n+                module.exit_json(changed=changed)\n+        \n+    elif mode == 'route':\n+        if not (dev and addr):    \n+            module.fail_json(msg='valid device and network required')\n+            \n+        routes = Routes.factoryRoutes()\n+        \n+        if state == \"absent\":\n+            if routes.has_route_any_gw(addr,dev):\n+                routes.del_route(addr,dev)\n+                module.exit_json(changed=True)\n+            else:\n+                module.exit_json(changed=False)\n+        else:\n+            if routes.has_route(addr,dev,via):\n+                module.exit_json(changed=False)\n+            elif routes.has_route_any_gw(addr,dev):\n+                # route exists but with different gateway\n+                # remove it before adding new route\n+                routes.del_route(addr,dev)\n+            \n+            routes.add_route(addr,dev,via)\n+            module.exit_json(changed=True)\n+                \n+    elif mode == 'link':\n+        \n+        # add an IF to a bridge\n+        if kind == 'port':\n+            \n+            if state==\"present\":\n+                if not (dev and link):    \n+                    module.fail_json(msg='valid device and bridge required')\n+                if not link.is_bridge():\n+                    module.fail_json(msg='bridge is not a bridge')\n+                    \n+                if dev.master == link.id:\n+                    module.exit_json(changed=False)\n+                else:\n+                    dev.set_master(link)\n+                    module.exit_json(changed=True)\n+            if state==\"absent\":","path":"network/ip.py","position":512,"original_position":512,"commit_id":"948c736818db9900ff81b1ade4ff7e7203e68996","original_commit_id":"948c736818db9900ff81b1ade4ff7e7203e68996","user":{"login":"adrianmoisey","id":736329,"avatar_url":"https://avatars.githubusercontent.com/u/736329?v=3","gravatar_id":"","url":"https://api.github.com/users/adrianmoisey","html_url":"https://github.com/adrianmoisey","followers_url":"https://api.github.com/users/adrianmoisey/followers","following_url":"https://api.github.com/users/adrianmoisey/following{/other_user}","gists_url":"https://api.github.com/users/adrianmoisey/gists{/gist_id}","starred_url":"https://api.github.com/users/adrianmoisey/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/adrianmoisey/subscriptions","organizations_url":"https://api.github.com/users/adrianmoisey/orgs","repos_url":"https://api.github.com/users/adrianmoisey/repos","events_url":"https://api.github.com/users/adrianmoisey/events{/privacy}","received_events_url":"https://api.github.com/users/adrianmoisey/received_events","type":"User","site_admin":false},"body":"Inconsistent spacing around the `==`","created_at":"2016-05-23T07:40:40Z","updated_at":"2016-05-23T07:40:40Z","html_url":"https://github.com/ansible/ansible-modules-extras/pull/2274#discussion_r64179634","pull_request_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274","_links":{"self":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments/64179634"},"html":{"href":"https://github.com/ansible/ansible-modules-extras/pull/2274#discussion_r64179634"},"pull_request":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274"}}},"pull_request":{"url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274","id":70964954,"html_url":"https://github.com/ansible/ansible-modules-extras/pull/2274","diff_url":"https://github.com/ansible/ansible-modules-extras/pull/2274.diff","patch_url":"https://github.com/ansible/ansible-modules-extras/pull/2274.patch","issue_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2274","number":2274,"state":"open","locked":false,"title":"ip module feature","user":{"login":"felix-engelmann","id":5196262,"avatar_url":"https://avatars.githubusercontent.com/u/5196262?v=3","gravatar_id":"","url":"https://api.github.com/users/felix-engelmann","html_url":"https://github.com/felix-engelmann","followers_url":"https://api.github.com/users/felix-engelmann/followers","following_url":"https://api.github.com/users/felix-engelmann/following{/other_user}","gists_url":"https://api.github.com/users/felix-engelmann/gists{/gist_id}","starred_url":"https://api.github.com/users/felix-engelmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/felix-engelmann/subscriptions","organizations_url":"https://api.github.com/users/felix-engelmann/orgs","repos_url":"https://api.github.com/users/felix-engelmann/repos","events_url":"https://api.github.com/users/felix-engelmann/events{/privacy}","received_events_url":"https://api.github.com/users/felix-engelmann/received_events","type":"User","site_admin":false},"body":"##### ISSUE TYPE\r\n<!--- Pick one below and delete the rest: -->\r\n - New Module Pull Request\r\n\r\n##### COMPONENT NAME\r\n<!--- Name of the plugin/module/task  -->\r\nip\r\n\r\n##### ANSIBLE VERSION\r\n```\r\nansible 2.0.1.0\r\n```\r\n\r\n##### SUMMARY\r\n<!--- Describe the change, including rationale and design decisions -->\r\n\r\nThe `ip` module aimes to provide the same functionality as the linux `ip` tool. Especially setting explicit routes which is not possible with the `nmcli` module. It uses the `netlink` socket which is abstracted by the `pyroute2` library. The module is capable of assigning and removing addresses to and from interfaces as well as managing routes.\r\n\r\nThe module is built with IPv6-first in focus, but is completely backwards compatible by automatic detection.\r\n\r\nWith further development, other functionality of the `ip` tool can be easily integrated to manage links and roles as well as refine the current operations by e.g. adding options for sophisticated routing. \r\n\r\nThis new feature is a bit dangerous, because one can easily cut the connection to the maschine by removing vital routes, but with care it is very powerful and allows to integrate the network management such as simple routing into ansible playbooks.\r\n\r\nIt removes the hacking with the `command` module which can not provide idempotency without files and is bad at distinguishing real changes from problems.\r\n\r\nI hope the general interest for this module is as high as mine and I would be happy to maintain and continue development for this module.\r\n\r\n<!---\r\nIf you are fixing an existing issue, please include \"Fixes #nnn\" in your\r\ncommit message and your description; but you should still explain what\r\nthe change does.\r\n-->\r\n","created_at":"2016-05-22T01:12:47Z","updated_at":"2016-05-23T07:40:40Z","closed_at":null,"merged_at":null,"merge_commit_sha":"7ab907ebbbd997cde114e5674f7c29d11e36c813","assignee":null,"milestone":null,"commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274/commits","review_comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274/comments","review_comment_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments{/number}","comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2274/comments","statuses_url":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/948c736818db9900ff81b1ade4ff7e7203e68996","head":{"label":"felix-engelmann:iproute_feature","ref":"iproute_feature","sha":"948c736818db9900ff81b1ade4ff7e7203e68996","user":{"login":"felix-engelmann","id":5196262,"avatar_url":"https://avatars.githubusercontent.com/u/5196262?v=3","gravatar_id":"","url":"https://api.github.com/users/felix-engelmann","html_url":"https://github.com/felix-engelmann","followers_url":"https://api.github.com/users/felix-engelmann/followers","following_url":"https://api.github.com/users/felix-engelmann/following{/other_user}","gists_url":"https://api.github.com/users/felix-engelmann/gists{/gist_id}","starred_url":"https://api.github.com/users/felix-engelmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/felix-engelmann/subscriptions","organizations_url":"https://api.github.com/users/felix-engelmann/orgs","repos_url":"https://api.github.com/users/felix-engelmann/repos","events_url":"https://api.github.com/users/felix-engelmann/events{/privacy}","received_events_url":"https://api.github.com/users/felix-engelmann/received_events","type":"User","site_admin":false},"repo":{"id":59375138,"name":"ansible-modules-extras","full_name":"felix-engelmann/ansible-modules-extras","owner":{"login":"felix-engelmann","id":5196262,"avatar_url":"https://avatars.githubusercontent.com/u/5196262?v=3","gravatar_id":"","url":"https://api.github.com/users/felix-engelmann","html_url":"https://github.com/felix-engelmann","followers_url":"https://api.github.com/users/felix-engelmann/followers","following_url":"https://api.github.com/users/felix-engelmann/following{/other_user}","gists_url":"https://api.github.com/users/felix-engelmann/gists{/gist_id}","starred_url":"https://api.github.com/users/felix-engelmann/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/felix-engelmann/subscriptions","organizations_url":"https://api.github.com/users/felix-engelmann/orgs","repos_url":"https://api.github.com/users/felix-engelmann/repos","events_url":"https://api.github.com/users/felix-engelmann/events{/privacy}","received_events_url":"https://api.github.com/users/felix-engelmann/received_events","type":"User","site_admin":false},"private":false,"html_url":"https://github.com/felix-engelmann/ansible-modules-extras","description":"Ansible extra modules - these modules ship with ansible","fork":true,"url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras","forks_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/forks","keys_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/keys{/key_id}","collaborators_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/teams","hooks_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/hooks","issue_events_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/issues/events{/number}","events_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/events","assignees_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/assignees{/user}","branches_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/branches{/branch}","tags_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/tags","blobs_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/git/refs{/sha}","trees_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/git/trees{/sha}","statuses_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/statuses/{sha}","languages_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/languages","stargazers_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/stargazers","contributors_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/contributors","subscribers_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/subscribers","subscription_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/subscription","commits_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/commits{/sha}","git_commits_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/git/commits{/sha}","comments_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/comments{/number}","issue_comment_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/issues/comments{/number}","contents_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/contents/{+path}","compare_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/compare/{base}...{head}","merges_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/merges","archive_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/downloads","issues_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/issues{/number}","pulls_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/pulls{/number}","milestones_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/milestones{/number}","notifications_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/labels{/name}","releases_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/releases{/id}","deployments_url":"https://api.github.com/repos/felix-engelmann/ansible-modules-extras/deployments","created_at":"2016-05-21T18:16:58Z","updated_at":"2016-05-21T18:17:02Z","pushed_at":"2016-05-23T01:41:38Z","git_url":"git://github.com/felix-engelmann/ansible-modules-extras.git","ssh_url":"git@github.com:felix-engelmann/ansible-modules-extras.git","clone_url":"https://github.com/felix-engelmann/ansible-modules-extras.git","svn_url":"https://github.com/felix-engelmann/ansible-modules-extras","homepage":"","size":7424,"stargazers_count":0,"watchers_count":0,"language":"Python","has_issues":false,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":0,"forks":0,"open_issues":0,"watchers":0,"default_branch":"devel"}},"base":{"label":"ansible:devel","ref":"devel","sha":"b11029adca11ef9ce15714e0b3cb47d468e4c0e2","user":{"login":"ansible","id":1507452,"avatar_url":"https://avatars.githubusercontent.com/u/1507452?v=3","gravatar_id":"","url":"https://api.github.com/users/ansible","html_url":"https://github.com/ansible","followers_url":"https://api.github.com/users/ansible/followers","following_url":"https://api.github.com/users/ansible/following{/other_user}","gists_url":"https://api.github.com/users/ansible/gists{/gist_id}","starred_url":"https://api.github.com/users/ansible/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ansible/subscriptions","organizations_url":"https://api.github.com/users/ansible/orgs","repos_url":"https://api.github.com/users/ansible/repos","events_url":"https://api.github.com/users/ansible/events{/privacy}","received_events_url":"https://api.github.com/users/ansible/received_events","type":"Organization","site_admin":false},"repo":{"id":24474138,"name":"ansible-modules-extras","full_name":"ansible/ansible-modules-extras","owner":{"login":"ansible","id":1507452,"avatar_url":"https://avatars.githubusercontent.com/u/1507452?v=3","gravatar_id":"","url":"https://api.github.com/users/ansible","html_url":"https://github.com/ansible","followers_url":"https://api.github.com/users/ansible/followers","following_url":"https://api.github.com/users/ansible/following{/other_user}","gists_url":"https://api.github.com/users/ansible/gists{/gist_id}","starred_url":"https://api.github.com/users/ansible/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/ansible/subscriptions","organizations_url":"https://api.github.com/users/ansible/orgs","repos_url":"https://api.github.com/users/ansible/repos","events_url":"https://api.github.com/users/ansible/events{/privacy}","received_events_url":"https://api.github.com/users/ansible/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/ansible/ansible-modules-extras","description":"Ansible extra modules - these modules ship with ansible","fork":false,"url":"https://api.github.com/repos/ansible/ansible-modules-extras","forks_url":"https://api.github.com/repos/ansible/ansible-modules-extras/forks","keys_url":"https://api.github.com/repos/ansible/ansible-modules-extras/keys{/key_id}","collaborators_url":"https://api.github.com/repos/ansible/ansible-modules-extras/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/ansible/ansible-modules-extras/teams","hooks_url":"https://api.github.com/repos/ansible/ansible-modules-extras/hooks","issue_events_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/events{/number}","events_url":"https://api.github.com/repos/ansible/ansible-modules-extras/events","assignees_url":"https://api.github.com/repos/ansible/ansible-modules-extras/assignees{/user}","branches_url":"https://api.github.com/repos/ansible/ansible-modules-extras/branches{/branch}","tags_url":"https://api.github.com/repos/ansible/ansible-modules-extras/tags","blobs_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/refs{/sha}","trees_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/trees{/sha}","statuses_url":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/{sha}","languages_url":"https://api.github.com/repos/ansible/ansible-modules-extras/languages","stargazers_url":"https://api.github.com/repos/ansible/ansible-modules-extras/stargazers","contributors_url":"https://api.github.com/repos/ansible/ansible-modules-extras/contributors","subscribers_url":"https://api.github.com/repos/ansible/ansible-modules-extras/subscribers","subscription_url":"https://api.github.com/repos/ansible/ansible-modules-extras/subscription","commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/commits{/sha}","git_commits_url":"https://api.github.com/repos/ansible/ansible-modules-extras/git/commits{/sha}","comments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/comments{/number}","issue_comment_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/comments{/number}","contents_url":"https://api.github.com/repos/ansible/ansible-modules-extras/contents/{+path}","compare_url":"https://api.github.com/repos/ansible/ansible-modules-extras/compare/{base}...{head}","merges_url":"https://api.github.com/repos/ansible/ansible-modules-extras/merges","archive_url":"https://api.github.com/repos/ansible/ansible-modules-extras/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/ansible/ansible-modules-extras/downloads","issues_url":"https://api.github.com/repos/ansible/ansible-modules-extras/issues{/number}","pulls_url":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls{/number}","milestones_url":"https://api.github.com/repos/ansible/ansible-modules-extras/milestones{/number}","notifications_url":"https://api.github.com/repos/ansible/ansible-modules-extras/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/ansible/ansible-modules-extras/labels{/name}","releases_url":"https://api.github.com/repos/ansible/ansible-modules-extras/releases{/id}","deployments_url":"https://api.github.com/repos/ansible/ansible-modules-extras/deployments","created_at":"2014-09-25T20:28:42Z","updated_at":"2016-05-23T02:52:15Z","pushed_at":"2016-05-23T07:29:26Z","git_url":"git://github.com/ansible/ansible-modules-extras.git","ssh_url":"git@github.com:ansible/ansible-modules-extras.git","clone_url":"https://github.com/ansible/ansible-modules-extras.git","svn_url":"https://github.com/ansible/ansible-modules-extras","homepage":"","size":14353,"stargazers_count":588,"watchers_count":588,"language":"Python","has_issues":true,"has_downloads":true,"has_wiki":false,"has_pages":false,"forks_count":1209,"mirror_url":null,"open_issues_count":590,"forks":1209,"open_issues":590,"watchers":588,"default_branch":"devel"}},"_links":{"self":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274"},"html":{"href":"https://github.com/ansible/ansible-modules-extras/pull/2274"},"issue":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2274"},"comments":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/issues/2274/comments"},"review_comments":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274/comments"},"review_comment":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/pulls/2274/commits"},"statuses":{"href":"https://api.github.com/repos/ansible/ansible-modules-extras/statuses/948c736818db9900ff81b1ade4ff7e7203e68996"}}}}
{"actor":{"display_login":"adrianmoisey"}}