diff --git a/roles/azure_edges/README.md b/roles/azure_edges/README.md index 718fc2d..71fc657 100644 --- a/roles/azure_edges/README.md +++ b/roles/azure_edges/README.md @@ -46,6 +46,7 @@ Variables with default values that can be overridden by the user: - `vbond_port`, `default_vbond_ip`: Default configurations for vBond. - `az_cedge_vm_size`: Default Azure VM size for cEdge instances. - `edge_instances`: List of cEdge instance configurations. If not provided, instances will be created based on PnP Portal information. +- `edges_parallel_deployment`: Azure virtual machines are created sequentially by default. Setting this parameter to true enables simultaneous creation of all Edge VMs by using Azure resource manager deployments ### Vars (`vars/main.yml`) diff --git a/roles/azure_edges/defaults/main.yml b/roles/azure_edges/defaults/main.yml index 7224173..d7b08ac 100644 --- a/roles/azure_edges/defaults/main.yml +++ b/roles/azure_edges/defaults/main.yml @@ -78,3 +78,4 @@ edge_instances: [] # If no edge instances configured, they will be automatically created # based on the PnP Portal information. # See `deployment_edges_config` to inspect result +edges_parallel_deployment: false diff --git a/roles/azure_edges/tasks/azure_deployment.yml b/roles/azure_edges/tasks/azure_deployment.yml new file mode 100644 index 0000000..4cf7cb8 --- /dev/null +++ b/roles/azure_edges/tasks/azure_deployment.yml @@ -0,0 +1,84 @@ +# Copyright 2024 Cisco Systems, Inc. and its affiliates +# GNU General Public License v3.0+ (see LICENSE or https://www.gnu.org/licenses/gpl-3.0.txt) + +--- + +- name: "Set vpn0_default_gateway fact from VPN 0 subnet value" + ansible.builtin.set_fact: + vpn0_default_gateway: "{{ vpn0_subnet | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }}" + vpn512_default_gateway: "{{ vpn512_subnet | ansible.utils.ipaddr('1') | ansible.utils.ipaddr('address') }}" + vars: + vpn0_subnet: "{{ az_subnets | json_query('[?VPN==`0` && type!=`cluster`].cidr | [0]') }}" + vpn512_subnet: "{{ az_subnets | json_query('[?VPN==`512` && type!=`cluster`].cidr | [0]') }}" + +- name: "Get info about NSG: {{ az_network_security_group }}" + azure.azcollection.azure_rm_securitygroup_info: + resource_group: "{{ az_resource_group }}" + name: "{{ az_network_security_group }}" + register: az_res_gr + +- name: "Template userdata file for cedge: {{ hostname }}" + ansible.builtin.template: + src: ./userdata_cedge.j2 # ./bond.j2 ./userdata_cedge.j2 + dest: "{{ userdata_cedge_path }}-{{ hostname }}" + mode: "0644" + vars: + hostname: "{{ instance_item.hostname | replace('_', '-') }}" + uuid: "{{ instance_item.uuid }}" + otp: "{{ instance_item.otp }}" + vbond: "{{ instance_item.vbond }}" + system_ip: "{{ instance_item.system_ip }}" + site_id: "{{ instance_item.site_id }}" + loop: "{{ edge_instances }}" + loop_control: + loop_var: instance_item + when: instance_item.hostname not in instances_info or not instances_info[instance_item.hostname] + +- name: Create Azure Deployment + azure_rm_deployment: + resource_group: "{{ az_resource_group }}" + name: "{{ az_resources_prefix }}-edges" + location: "{{ az_location }}" + wait_for_deployment_completion: true + template: + $schema: "https://schema.management.azure.com/schemas/2015-01-01/deploymentTemplate.json#" + contentVersion: "1.0.0.0" + resources: "{{ lookup('template', 'deployment_resources.j2') }}" + outputs: "{{ lookup('template', 'deployment_outputs.j2') }}" + register: azure_deployment + +- name: "Extend Network Security Group for machine, NSG: {{ az_network_security_group }}" + azure.azcollection.azure_rm_securitygroup: + resource_group: "{{ az_resource_group }}" + name: "{{ az_network_security_group }}" + rules: "{{ lookup('template', 'nsg_rules.j2') }}" + tags: + Name: "{{ az_network_security_group }}" + Creator: "{{ az_tag_creator }}" + Organization: "{{ organization_name }}" + vars: + deployed_ips: "{{ azure_deployment['deployment']['outputs']['public_ip_addresses']['value'] | map(attribute='ip') }}" + existing_nsgs: "{{ az_res_gr.securitygroups | map(attribute='rules') | flatten | map(attribute='source_address_prefix') | list }}" + when: deployed_ips | difference(existing_nsgs) + +- name: Update deployment facts - cedge - that will be consumed by vManage-client in Ansible + ansible.builtin.set_fact: + deployment_facts: + deployed_edge_instances: "{{ deployment_facts.deployed_edge_instances + [instance] }}" + vars: + instance: + hostname: "{{ instance_item['hostname'] }}" + system_ip: "{{ instance_item['system_ip'] }}" + admin_username: "{{ admin_username }}" + admin_password: "{{ admin_password }}" + mgmt_public_ip: "{{ all_public_ips | json_query('[?host==`'~instance_item['hostname']~'` && type==`mgmt`].ip | [0]') }}" + transport_public_ip: "{{ all_public_ips | json_query('[?host==`'~instance_item['hostname']~'` && type==`transport`].ip | [0]') }}" + service_interfaces: "{{ service_interfaces if 'service' in az_subnets | map(attribute='type') else omit }}" + uuid: "{{ instance_item['uuid'] }}" + site_id: "{{ instance_item['site_id'] }}" + all_public_ips: "{{ azure_deployment['deployment']['outputs']['public_ip_addresses']['value'] }}" + service_interfaces: "{{ azure_deployment['deployment']['outputs']['service_interfaces']['value'][instance_item['hostname']] }}" + loop: "{{ edge_instances }}" + loop_control: + loop_var: instance_item + when: instance_item.hostname not in instances_info or not instances_info[instance_item.hostname] diff --git a/roles/azure_edges/tasks/main.yml b/roles/azure_edges/tasks/main.yml index f61e244..861d841 100644 --- a/roles/azure_edges/tasks/main.yml +++ b/roles/azure_edges/tasks/main.yml @@ -57,7 +57,14 @@ loop: "{{ edge_instances }}" loop_control: loop_var: instance_item - when: edge_instances is defined and (instance_item.hostname not in instances_info or not instances_info[instance_item.hostname]) + when: + - not edges_parallel_deployment + - edge_instances is defined + - instance_item.hostname not in instances_info or not instances_info[instance_item.hostname] + +- name: Create azure resource manager deployment - cEdge (C8000V) + ansible.builtin.include_tasks: azure_deployment.yml + when: edges_parallel_deployment - name: Extract deployment facts ansible.builtin.include_role: diff --git a/roles/azure_edges/templates/deployment_network_interfaces.j2 b/roles/azure_edges/templates/deployment_network_interfaces.j2 new file mode 100644 index 0000000..d7bbf13 --- /dev/null +++ b/roles/azure_edges/templates/deployment_network_interfaces.j2 @@ -0,0 +1,26 @@ +- type: "Microsoft.Network/networkInterfaces" + apiVersion: "2024-05-01" + name: "nic-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}" + location: "{{ az_location }}" + dependsOn: + - "[resourceId('Microsoft.Network/publicIPAddresses', 'public-ip-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}')]" + properties: + ipConfigurations: + - name: "ipconfig-vpn-{{ subnet_item.VPN }}" + properties: + primary: true + privateIPAllocationMethod: "Dynamic" +{% if subnet_item.type in ("mgmt", "transport") %} + publicIPAddress: + id: "[resourceId('Microsoft.Network/publicIPAddresses', 'public-ip-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}')]" +{% endif %} + subnet: + id: "{{ (az_virtualnetwork_info['virtualnetworks'][0]['subnets'] | json_query('[?name==`' ~ subnet_item.name ~ '`].id'))[0] }}" + networkSecurityGroup: + id: "{{ az_res_gr['securitygroups'][0]['id'] }}" + tags: + Name: "nic-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}" + Creator: "{{ az_tag_creator }}" + Organization: "{{ organization_name }}" + VPN: "{{ subnet_item.VPN }}" + type: "{{ subnet_item.type }}" diff --git a/roles/azure_edges/templates/deployment_outputs.j2 b/roles/azure_edges/templates/deployment_outputs.j2 new file mode 100644 index 0000000..477446c --- /dev/null +++ b/roles/azure_edges/templates/deployment_outputs.j2 @@ -0,0 +1,27 @@ +{% filter from_yaml %} +public_ip_addresses: + type: array + value: +{% for instance in edge_instances if instance.hostname not in instances_info %} +{% for subnet_item in az_subnets if subnet_item.type in ("mgmt", "transport") %} + - name: public-ip-{{ instance.hostname }}-vpn-{{ subnet_item.VPN }} + ip: "[reference('public-ip-{{ instance.hostname }}-vpn-{{ subnet_item.VPN }}').ipAddress]" + type: {{ subnet_item.type }} + host: {{ instance.hostname }} +{% endfor %} +{% endfor %} +service_interfaces: + type: object + value: +{% for instance in edge_instances if instance.hostname not in instances_info %} +{% if "service" in az_subnets | map(attribute='type') %} + {{ instance['hostname'] }}: +{% for subnet_item in az_subnets if subnet_item.type in ("service") %} + addr: "[reference('nic-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}').ipConfigurations[0].privateIPAddress]" + index: {{ loop.index0 + 2}} +{% endfor %} +{% else %} + {{ instance['hostname'] }}: {} +{% endif %} +{% endfor %} +{% endfilter %} diff --git a/roles/azure_edges/templates/deployment_public_ip_addresses.j2 b/roles/azure_edges/templates/deployment_public_ip_addresses.j2 new file mode 100644 index 0000000..6a63caa --- /dev/null +++ b/roles/azure_edges/templates/deployment_public_ip_addresses.j2 @@ -0,0 +1,13 @@ +- type: "Microsoft.Network/publicIPAddresses" + apiVersion: "2024-05-01" + name: "public-ip-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}" + location: "{{ az_location }}" + properties: + publicIPAllocationMethod: "Static" + tags: + Name: "public-ip-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}" + Creator: "{{ az_tag_creator }}" + Machine: "{{ instance_item['hostname'] }}" + VPN: "{{ subnet_item.VPN }}" + Subnet: "{{ subnet_item.name }}" + type: "{{ subnet_item.type }}" diff --git a/roles/azure_edges/templates/deployment_resources.j2 b/roles/azure_edges/templates/deployment_resources.j2 new file mode 100644 index 0000000..e64f47b --- /dev/null +++ b/roles/azure_edges/templates/deployment_resources.j2 @@ -0,0 +1,14 @@ +{% filter from_yaml %} +{% for instance_item in edge_instances if instance_item.hostname not in instances_info %} +{% for subnet_item in az_subnets %} +{% if subnet_item.type in ("mgmt", "transport", "service") %} +{% include "deployment_public_ip_addresses.j2" %} + +{% include "deployment_network_interfaces.j2" %} + +{% endif %} +{% endfor %} +{% include "deployment_virtual_machines.j2" %} + +{% endfor %} +{% endfilter %} diff --git a/roles/azure_edges/templates/deployment_virtual_machines.j2 b/roles/azure_edges/templates/deployment_virtual_machines.j2 new file mode 100644 index 0000000..2bfacd9 --- /dev/null +++ b/roles/azure_edges/templates/deployment_virtual_machines.j2 @@ -0,0 +1,46 @@ +- type: "Microsoft.Compute/virtualMachines" + apiVersion: "2024-11-01" + name: "{{ instance_item['hostname'] }}" + location: "{{ az_location }}" + dependsOn: +{% for subnet_item in az_subnets %} + - "[resourceId('Microsoft.Network/networkInterfaces', 'nic-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}')]" +{% endfor %} + plan: + name: "{{ az_cedge_image_sku }}" + product: "{{ az_cedge_image_offer }}" + publisher: "{{ az_cedge_image_publisher }}" + properties: + diagnosticsProfile: + bootDiagnostics: + enabled: true + hardwareProfile: + vmSize: "{{ az_cedge_vm_size }}" + osProfile: + adminUsername: "{{ admin_username }}-tmp" + adminPassword: "{{ admin_password }}" + computerName: "{{ instance_item['hostname'] }}" + linuxConfiguration: + disablePasswordAuthentication: false + customData: "{{ lookup('file', userdata_cedge_path ~ '-' ~ instance_item['hostname']) | b64encode }}" + storageProfile: + imageReference: + publisher: "{{ az_cedge_image_publisher }}" + offer: "{{ az_cedge_image_offer }}" + sku: "{{ az_cedge_image_sku }}" + version: "{{ az_cedge_image_version }}" + osDisk: + name: "{{ instance_item['hostname'] }}-os-disk" + createOption: FromImage + diskSizeGB: 30 + osType: "Linux" + caching: "ReadWrite" + managedDisk: + storageAccountType: "StandardSSD_ZRS" + networkProfile: + networkInterfaces: +{% for subnet_item in az_subnets %} + - id: "[resourceId('Microsoft.Network/networkInterfaces', 'nic-{{ instance_item['hostname'] }}-vpn-{{ subnet_item.VPN }}')]" + properties: + primary: {{ subnet_item.type == "mgmt" | string }} +{% endfor %} diff --git a/roles/azure_edges/templates/nsg_rules.j2 b/roles/azure_edges/templates/nsg_rules.j2 new file mode 100644 index 0000000..34953c3 --- /dev/null +++ b/roles/azure_edges/templates/nsg_rules.j2 @@ -0,0 +1,13 @@ +{% filter from_yaml %} +--- +{% for public_ip in azure_deployment['deployment']['outputs']['public_ip_addresses']['value'] if public_ip['name'] not in az_res_gr.securitygroups | map(attribute='rules') | flatten | map(attribute='name') | list %} +- name: "{{ public_ip['name'] }}" + protocol: "*" + destination_port_range: "*" + source_port_range: "*" + source_address_prefix: "{{ public_ip['ip'] }}" + access: Allow + priority: "{{ 1500 + ((az_res_gr.securitygroups | first).rules | length) + loop.index }}" + direction: Inbound +{% endfor %} +{% endfilter %}