{"id":22178155,"url":"https://github.com/cmason3/cloud","last_synced_at":"2025-07-26T17:31:21.337Z","repository":{"id":156422452,"uuid":"633011871","full_name":"cmason3/cloud","owner":"cmason3","description":"How I Automated a Cloud","archived":false,"fork":false,"pushed_at":"2023-10-08T06:05:13.000Z","size":64,"stargazers_count":1,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2023-10-08T08:33:39.134Z","etag":null,"topics":["ansible","jinja2","juniper","junos-automation","python"],"latest_commit_sha":null,"homepage":"","language":"Jinja","has_issues":false,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/cmason3.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null}},"created_at":"2023-04-26T15:34:30.000Z","updated_at":"2023-10-08T06:44:13.000Z","dependencies_parsed_at":null,"dependency_job_id":"1c066ec7-bb59-4d1b-ae20-b47798414311","html_url":"https://github.com/cmason3/cloud","commit_stats":null,"previous_names":[],"tags_count":0,"template":null,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmason3%2Fcloud","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmason3%2Fcloud/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmason3%2Fcloud/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/cmason3%2Fcloud/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/cmason3","download_url":"https://codeload.github.com/cmason3/cloud/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":227699321,"owners_count":17806354,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["ansible","jinja2","juniper","junos-automation","python"],"created_at":"2024-12-02T08:45:38.811Z","updated_at":"2024-12-02T08:45:39.397Z","avatar_url":"https://github.com/cmason3.png","language":"Jinja","funding_links":[],"categories":[],"sub_categories":[],"readme":"# How I Automated a Cloud\n\nNot quite a whole Cloud, but a Juniper based Leaf/Spine EVPN fabric that supports a Red Hat OpenStack deployment. This repository contains a framework as opposed to a full deployment but gives the general idea as to how we used Ansible with Jinja2 to build and deploy the networking aspects of our Cloud environment. Whilst we have used Ansible before, we had never used it on this scale - we wanted Ansible to deploy everything, with no need for manual configuration (Network as Code).\n\nThe initial challenge was how do we get all the information about interfaces, IP addresses and BGP ASNs into the Jinja2 templates that we were going to use to generate our device configurations. Anyone who is familiar with Ansible knows information is provided in `group_vars` and `host_vars`, but when you have nearly a hundred devices per environment you don't want to be creating and updating them by hand. Excel is quite common with Engineers that deploy networks as it is usually used for patching schedules, which detail what needs to be connected to what. As we didn’t want to duplicate information in multiple places, we wanted to use this information as input to our Ansible automation, but it didn't contain everything we needed.\n\nOne important point that I can't emphasise enough when designing a large deployment, is to design it with automation in mind. Without standardisation you can't easily template it, which is key to automation unless you want lots and lots of lookup tables. Make things deterministic as much as possible, for example, design it so that leaf-01 connects to port 1 and leaf-02 connects to port 2 on your spine switches, allocate a point-to-point subnet per spine switch and use the leaf number to determine the subnet to use and allocate BGP ASNs based on your leaf or spine numbers. When allocating anything, think how can I devise a function that can work it out without having to perform a lookup.\n\nWe wanted to make it easy for someone to describe a whole environment without having to modify lots of different files, so we came up with the approach of using two files per environment (a CSV file that was based on a patching schedule and a YAML file which allowed us to specify subnets and other important bits of information). These two files were stored in a Git repository so they were version controlled and could be used to trigger a CI/CD pipeline if we decided it was ever a good idea to automatically push changes en masse to a Live production environment.\n\nInstead of writing a throwaway Python script which parsed these two files (as lots of people have done), we decided to write a generic tool [JinjaFx](https://pypi.org/project/jinjafx/) which took CSV and/or YAML as input and combined it with a Jinja2 template to generate our `group_vars` and `host_vars` that Ansible would then use:\n\n```\n.\n└── /contrib\n    ├── GenerateSiteVars.j2\n    ├── site_a.csv\n    └── site_a.yml\n```\n\nWith these files, we can then run the following commands:\n\n```\npython3 -m pip install --user jinjafx\n \npython3 -m jinjafx -g contrib/site_a.yml -d contrib/site_a.csv -t contrib/GenerateSiteVars.j2\n```\n\nThis results in the below files being generated - the `GenerateSiteVars.j2` template will need to be tailored to your environment as it currently only adds a subset of the information required to deploy all the networking aspects. If you look at `site_a.csv` you will also notice it doesn't include all the connections required - JinjaFx supports something that I term dynamic CSV, where we can use counters and RegEx style character classes and groups to expand rows into lots of rows if there is a pattern - the following CSV will get expanded to over 200 lines:\n\nDEVICE | INTERFACE | HOST | PORT | TAG\n--- | --- | --- | --- | ---\n`mx-{1-2:1}%2` | `et-0/1/\\1` | `spine-({1-3:1})%2` | `et-0/0/{0\\|63:2}` | `Underlay`\n`spine-({1-3:1})%2` | `et-0/0/{0\\|63}` | `mx-{1-2:1}%2` | `et-0/1/\\1` | `Underlay`\n`leaf-({1-32:1})%2` | `et-0/0/\\2` | `spine-({1-3:1})%2` | `et-0/0/\\1` | `Underlay`\n`spine-({1-3:1})%2` | `et-0/0/\\2` | `leaf-({1-32:1})%2` | `et-0/0/\\1` | `Underlay`\n\nThe syntax `{1-32:1}` is a counter which will create rows using the values 1 to 32 with an increment of 1. The `%2` syntax will ensure the number is zero padded to 2 digits. The `{0|63}` syntax will alternate the value between 0 and 63 for subsequent rows. The `()` and `\\n` syntax are standard RegEx style capture groups that allows you to copy a value and use it elsewhere.\n\nYou should never modify anything within the `sites` directory - only ever modify the two site-specific files within the `contrib` directory and then re-run the above command to re-generate the files within the `sites` directory.\n\n```\n894B \u003e sites/site_a/hosts.yml\n49B \u003e sites/site_a/group_vars/all.yml\n429B \u003e sites/site_a/host_vars/ukabc-prd-mx-01.yml\n432B \u003e sites/site_a/host_vars/ukabc-prd-mx-02.yml\n4.73kB \u003e sites/site_a/host_vars/ukabc-prd-spine-01.yml\n4.77kB \u003e sites/site_a/host_vars/ukabc-prd-spine-02.yml\n4.74kB \u003e sites/site_a/host_vars/ukabc-prd-spine-03.yml\n444B \u003e sites/site_a/host_vars/ukabc-prd-leaf-01.yml\n444B \u003e sites/site_a/host_vars/ukabc-prd-leaf-02.yml\n...\n449B \u003e sites/site_a/host_vars/ukabc-prd-leaf-31.yml\n449B \u003e sites/site_a/host_vars/ukabc-prd-leaf-32.yml\n```\n\nNow we have a way to generate our site-specific `group_vars` and `host_vars` on demand, we need to look at how we are going to structure all of this. One of the good features of Ansible is the ability to define `group_vars` and `host_vars` at different levels to create global ones and site-specific ones:\n\n```\n.\n├── playbook-build.yml\n├── playbook-deploy.yml\n|\n├── /group_vars\n│   ├── credentials.yml\n│   ├── all.yml\n│   ├── leaf.yml\n│   ├── spine.yml\n|   └── mx.yml\n|\n└── /sites\n    └── /site_a\n        ├── hosts.yml\n        |\n        ├── /group_vars\n        │   ├── credentials.yml\n        │   └── all.yml\n        |\n        └── /host_vars\n            └── \u003chostname\u003e.yml\n```\n\nAnsible will first process `group_vars` and `host_vars` at the same level as our Playbooks and then it will process `group_vars` and `host_vars` at the same level as the site-specific inventory `hosts.yml`. Anything that is defined in `group_vars/all.yml` can be overridden by defining the same variable within `sites/site_a/group_vars/all.yml`. We also have global `group_vars` that define variables that are specific to the device group (i.e. \"leaf\", \"spine\" or \"mx\") as defined in the inventory.\n\nWe have defined `credentials.yml` at both the global level and site specific level - this allows you to define global credentials and then override them at site level if you need. This file should be Ansible Vault encrypted and is imported as part of `playbook-build.yml`. It goes without saying that you should never store unencrypted passwords, keys or secrets within a repository.\n\nThe final piece of the puzzle was our Jinja2 templates used to generate our device configurations which we place within a `templates` directory (the main templates will include different re-usable bits of configuration from the `includes` directory):\n\n```\n.\n└── /templates\n    ├── fabric.j2\n    |\n    └── /includes\n        └── junos.\u003c*\u003e.j2\n```\n\nThe Ansible Playbook `playbook-build.yml` defines a build task using the `template` module - the required template is defined within `group_vars/leaf.yml`, `group_vars/spine.yml` or `group_vars/mx.yml` depending on the device type:\n\n```yaml\ntemplate: fabric.j2\n```\n\nThis template will then import other templates from the `templates/includes` directory as required. These templates will reference variables which are defined within the `group_vars` and `host_vars`.\n\n```yaml\n- hosts: all\n  connection: local\n  gather_facts: no\n\n  tasks:\n    - name: \"create local build directory\"\n      file:\n        path: \"build/\"\n        state: directory\n      run_once: True\n\n    - name: \"include vaulted credentials (global)\"\n      include_vars: \"group_vars/credentials.yml\"\n      failed_when: false\n      run_once: true\n\n    - name: \"include vaulted credentials (site specific)\"\n      include_vars: \"{{ inventory_dir }}/group_vars/credentials.yml\"\n      failed_when: false\n      run_once: true\n\n    - name: \"build device configurations\"\n      template:\n        src: \"templates/{{ template }}\"\n        dest: \"build/{{ inventory_hostname }}.cfg\"\n```\n\nBy running the following command it will build all the configurations for a specific site:\n\n```\nansible-playbook playbook-build.yml -i sites/site_a/hosts.yml [--ask-vault-pass]\n```\n\nWe then used the `juniper.device.config` module from the `juniper.device` Ansible collection in the following Playbook to deploy our configuration to our Juniper devices over NETCONF - we started off by deploying the configuration using `load: replace` so we automated subsets of it, but we have now migrated to using `load: override` as all network configuration is generated by our Ansible templates (this is defined as `deploy_method` within our `group_vars`).\n\n```yaml\n- hosts: all\n  connection: local\n  gather_facts: no\n\n  tasks:\n    - name: \"junos commit confirmed\"\n      juniper.device.config:\n        load: \"{{ deploy_method }}\"\n        format: \"text\"\n        src: \"build/{{ inventory_hostname }}.cfg\"\n        check_commit_wait: 10\n        confirmed: 5\n        diff: true\n      register: response\n\n    - name: \"diff\"\n      debug: var=response.diff_lines\n      when: response.diff_lines is defined\n\n    - name: \"junos commit\"\n      juniper.device.config:\n        commit: true\n      when: response.diff_lines is defined\n```\n\nWe then run the following command to deploy the changes for a specific site:\n\n```\nansible-playbook playbook-deploy.yml -i sites/site_a/hosts.yml -u \u003cUSER\u003e -k\n```\n\nWe also use the `junos.device.config` module with the `check: true` flag for a check stage, which confirms what changes will be made without actually deploying them. If you wish to rollback the changes you can also use the `junos.device.config` module with the `rollback: 1` flag.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmason3%2Fcloud","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fcmason3%2Fcloud","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fcmason3%2Fcloud/lists"}