{"id":396,"date":"2021-02-20T03:00:00","date_gmt":"2021-02-20T08:00:00","guid":{"rendered":"https:\/\/chasberndt.com\/?p=396"},"modified":"2021-02-20T12:15:52","modified_gmt":"2021-02-20T17:15:52","slug":"creating-a-dynamic-inventory-script-for-ansible","status":"publish","type":"post","link":"https:\/\/chasberndt.com\/?p=396","title":{"rendered":"Creating a Dynamic Inventory Script for Ansible"},"content":{"rendered":"\n<p>It seems no one has written a blog post on creating dynamic inventory scripts for Ansible in a while. I feel this topic could use an update as some of the information I found was incomplete or out of date.<\/p>\n\n\n\n<p>My goal is was convert Terraforms&#8217;s tfstate data from DigitalOcean to a usable inventory script. Keep that in mind as it drove many specifics on how the script works. I want to also note that the script I reference is a first pass at getting a working inventory script.<\/p>\n\n\n\n<p>So first, the script (in its current state):<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>#!\/usr\/bin\/python3\n\nimport subprocess\nimport argparse\nimport json\n\nrelevant_tf_state_values = {\n    'digitalocean_droplet': &#91;'name', 'ipv4_address', 'ipv4_address_private', 'tags'],\n    'digitalocean_database_cluster': &#91;'name', 'host', 'private_host', 'port'],\n    'digitalocean_database_user': &#91;'name', 'password'],\n    'digitalocean_database': &#91;'name'],\n    'digitalocean_domain': &#91;'id'],\n    'digitalocean_volume': &#91;'name', 'size', 'initial_filesystem_type'],\n    'digitalocean_ssh_key': &#91;'name', 'fingerprint']\n}\n\nextra_vars = {\n    'ansible_ssh_user': 'root',\n    'web_mount_point': '\/mnt\/nfs\/data',\n    'web_mount_point_type': 'nfs',\n    'ansible_ssh_common_args': '-o StrictHostKeyChecking=no -o userknownhostsfile=\/dev\/null'\n}\n\nclass DigitalOceanInventory(object):\n\n    def __init__(self):\n        self.tags = &#91;]\n        self.droplets = &#91;]\n        self.vars = {}\n        self.inventory_json = json.loads(self._get_terraform_output())\n        self._generate_groups()\n        self._generate_vars()\n        self.ansible_inventory = self._generate_ansible_inventory()\n    \n    def _get_terraform_output(self):\n        process = subprocess.Popen(&#91;'terraform', 'show', '-json'],\n                                   stdout=subprocess.PIPE,\n                                   stderr=subprocess.PIPE,\n                                   universal_newlines=True)\n        stdout, stderr = process.communicate()\n        return stdout\n\n    def _parse_resource(self, resource, resource_type, relevant_objects):\n        data = {}\n        for key, value in resource&#91;'values'].items():\n            if key in relevant_objects:\n                data&#91;f'{resource_type}_{key}'] = value\n        return data\n\n    def _generate_groups(self):\n        tags = 'digitalocean_tag'\n        droplets = 'digitalocean_droplet'\n        for resource in self.inventory_json&#91;'values']&#91;'root_module']&#91;'resources']:\n            if resource&#91;'type'] == tags:\n                self.tags.append(resource&#91;'values']&#91;'name'])\n            elif resource&#91;'type'] == droplets:\n                self.droplets.append(self._parse_resource(resource, droplets, relevant_tf_state_values&#91;droplets]))\n\n    def _generate_vars(self):\n        for resource in self.inventory_json&#91;'values']&#91;'root_module']&#91;'resources']:\n            if resource&#91;'type'] in relevant_tf_state_values.keys() and resource&#91;'type'] not in \\\n                    &#91;'digitalocean_tags', 'digitalocean_droplets']:\n                for key, value in resource&#91;'values'].items():\n                    if key in relevant_tf_state_values&#91;resource&#91;'type']] and key not in &#91;'ip', 'tags']:\n                        resource_id = resource&#91;'type']\n                        self.vars&#91;f'{resource_id}_{key}'] = value\n                for key, value in extra_vars.items():\n                    self.vars&#91;key] = value\n\n    def _generate_ansible_inventory(self):\n        inventory = {}\n        for tag in self.tags:\n            hosts = &#91;]\n            public_ips = &#91;]\n            private_ips = &#91;]\n            inventory&#91;tag] = {}\n            for droplet in self.droplets:\n                if tag in droplet&#91;'digitalocean_droplet_tags']:\n                    hosts.append(droplet&#91;'digitalocean_droplet_ipv4_address'])\n                    public_ips.append(droplet&#91;'digitalocean_droplet_ipv4_address'])\n                    private_ips.append(droplet&#91;'digitalocean_droplet_ipv4_address_private'])\n                inventory&#91;tag]&#91;'hosts'] = hosts\n                inventory&#91;tag]&#91;'vars'] = self.vars\n            ansible_tag = tag.replace('-', '_')\n            inventory&#91;tag]&#91;'vars']&#91;f'{ansible_tag}_public_ips'] = public_ips\n            inventory&#91;tag]&#91;'vars']&#91;f'{ansible_tag}_private_ips'] = private_ips\n            if 'digitalocean_volume_name' in inventory&#91;tag]&#91;'vars']:\n                nfs_mount_point = str('\/mnt\/' + inventory&#91;tag]&#91;'vars']&#91;'digitalocean_volume_name'].replace('-', '_'))\n                inventory&#91;tag]&#91;'vars']&#91;'nfs_mount_point'] = nfs_mount_point\n        inventory&#91;'_meta'] = {}\n        inventory&#91;'_meta']&#91;'hostvars'] = {}\n        return inventory\n\n    def get_inventory(self):\n        return json.dumps(self.ansible_inventory, indent=2)\n\n\ndef main():\n    parser = argparse.ArgumentParser()\n    parser.add_argument('--save', '-s', help='Generates Ansible inventory and stores to disk as inventory.json.',\n                        action='store_true')\n    parser.add_argument('--list', action='store_true')\n    args = parser.parse_args()\n    do = DigitalOceanInventory()\n    if args.list:\n        print(do.get_inventory())\n    elif args.save:\n        with open('inventory.json', 'w') as inventory:\n            inventory.write(do.get_inventory())\n\n\nif __name__ == '__main__':\n    main()\n<\/code><\/pre>\n\n\n\n<p>At a high level, we&#8217;re getting the tfstate from Terraform by running the following command:<code> terraform show -json<\/code>. Next, we generate hostgroups by piggybacking on the tags added to host resources during creation. Next, we parse through the other resources to get the subset of information that we&#8217;re interested in. Finally, we generate an Python object with all the data in the desired format. Finally, we dump it as a JSON object and either return it to stdout or to <code>inventory.json<\/code>.<\/p>\n\n\n\n<p>The inventory output looks something like this:<\/p>\n\n\n\n<pre class=\"wp-block-code\"><code>{\n  \"tag-name-node\": {\n    \"hosts\": &#91;\n      \"10.0.0.1\"\n    ],\n    \"vars\": {\n      \"digitalocean_ssh_key_fingerprint\": \"00:11:22:33:44:55:66:77:88:99:AA:BB:CC:DD:EE:FF\",\n      \"digitalocean_ssh_key_name\": \"sshkeyname\",\n      \"ansible_ssh_user\": \"root\",\n      \"web_mount_point\": \"\/mnt\/nfs\/data\",\n      \"web_mount_point_type\": \"nfs\",\n      \"ansible_ssh_common_args\": \"-o StrictHostKeyChecking=no -o userknownhostsfile=\/dev\/null\",\n      \"digitalocean_database_cluster_host\": \"something.ondigitalocean.com\",\n      \"digitalocean_database_cluster_name\": \"db-name\",\n      \"digitalocean_database_cluster_port\": 25060,\n      \"digitalocean_database_cluster_private_host\": \"private.something.ondigitalocean.com\",\n      \"digitalocean_database_user_name\": \"wordpress\",\n      \"digitalocean_database_user_password\": \"password\",\n      \"digitalocean_domain_id\": \"something.com\",\n      \"digitalocean_volume_initial_filesystem_type\": \"ext4\",\n      \"digitalocean_volume_name\": \"volume-name\",\n      \"digitalocean_volume_size\": 5,\n      \"nfs_node_public_ips\": &#91;\n        \"10.0.0.1\"\n      ],\n      \"nfs_node_private_ips\": &#91;\n        \"10.0.0.1\"\n      ],\n      \"nfs_mount_point\": \"\/mnt\/barista_cloud_volume\"\n    }\n  },\n  \"_meta\": {\n    \"hostvars\": {}\n  }\n}<\/code><\/pre>\n\n\n\n<p>Now, if you try to feed this to Ansible as an inventory file, it will not be parsed correctly. <strong>The dynamic inventory JSON format is not the same as the JSON inventory format.<\/strong> This took me awhile to figure out and is honestly kind of frustrating as it makes creating a working JSON template so you can iterate and test quickly much more difficult than it needs to be. On the topic of gotcha&#8217;s, here a a few more to be aware of.<\/p>\n\n\n\n<ol class=\"wp-block-list\"><li>Your inventory script does not have to be written in Python, but it must include a shebang at the top of the script so it can be executed (also it must be executable so <code>chmod +x<\/code> your script).<\/li><li>The inventory script must accept the flag <code>--list<\/code>. It&#8217;s supposed to also accept <code>--host<\/code> and return details on a single host but I have not needed it nor implemented it.<\/li><li>Even if you are not adding vars for specific hosts, you MUST include the <code>_meta<\/code> section in your inventory.<\/li><\/ol>\n\n\n\n<p>That&#8217;s about it. I will probably come back around and clean this script up and make it more reusable. Heck, I might put together a boilerplate script that can make creating custom dynamic inventory scripts quicker. As mentioned before, this is a first pass attempt to get something that works for my use case.<\/p>\n\n\n\n<p>Finally, I feel I would be remiss if I did not include the tidbits of info I found scattered around the web that helped me figure this out. <\/p>\n\n\n\n<p><a href=\"https:\/\/www.jeffgeerling.com\/blog\/creating-custom-dynamic-inventories-ansible\" target=\"_blank\" rel=\"noreferrer noopener\">https:\/\/www.jeffgeerling.com\/blog\/creating-custom-dynamic-inventories-ansible<\/a> (Jeff, as always, is an invaluable resource on all things Ansible.)<\/p>\n\n\n\n<p><a href=\"https:\/\/docs.ansible.com\/ansible\/2.9\/dev_guide\/developing_inventory.html\">https:\/\/docs.ansible.com\/ansible\/2.9\/dev_guide\/developing_inventory.html<\/a><\/p>\n\n\n\n<p><a href=\"https:\/\/adamj.eu\/tech\/2016\/12\/04\/writing-a-custom-ansible-dynamic-inventory-script\/\">https:\/\/adamj.eu\/tech\/2016\/12\/04\/writing-a-custom-ansible-dynamic-inventory-script\/<\/a><\/p>\n\n\n\n<p>Thanks all folks. Have a good weekend!<\/p>\n","protected":false},"excerpt":{"rendered":"<p>It seems no one has written a blog post on creating dynamic inventory scripts for Ansible in a while. I feel this topic could use an update as some of the information I found was incomplete or out of date. My goal is was convert Terraforms&#8217;s tfstate data from DigitalOcean to a usable inventory script. <a class=\"read-more\" href=\"https:\/\/chasberndt.com\/?p=396\">&hellip;&nbsp;<span class=\"meta-nav\">&rarr;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":{"_jetpack_memberships_contains_paid_content":false,"footnotes":""},"categories":[20,13,19,22],"tags":[],"class_list":["post-396","post","type-post","status-publish","format-standard","hentry","category-ansible","category-automation","category-cloud","category-terraform"],"jetpack_featured_media_url":"","jetpack_sharing_enabled":true,"_links":{"self":[{"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/posts\/396","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/chasberndt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcomments&post=396"}],"version-history":[{"count":1,"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/posts\/396\/revisions"}],"predecessor-version":[{"id":397,"href":"https:\/\/chasberndt.com\/index.php?rest_route=\/wp\/v2\/posts\/396\/revisions\/397"}],"wp:attachment":[{"href":"https:\/\/chasberndt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fmedia&parent=396"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/chasberndt.com\/index.php?rest_route=%2Fwp%2Fv2%2Fcategories&post=396"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/chasberndt.com\/index.php?rest_route=%2Fwp%2Fv2%2Ftags&post=396"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}