|
| 1 | +from typing import List, Dict |
| 2 | + |
| 3 | +from cloudshell.api.cloudshell_api import InputNameValue, CloudShellAPISession, Connector, VmDetailsNetworkInterface |
| 4 | +import json |
| 5 | + |
| 6 | +from cloudshell.helpers.sandbox_reporter.reporter import SandboxReporter |
| 7 | + |
| 8 | + |
| 9 | +class SetAzureRoutesError(Exception): |
| 10 | + pass |
| 11 | + |
| 12 | + |
| 13 | +class Route: |
| 14 | + def __init__(self, name: str, address_prefix: str, next_hop_address: str, next_hop_type="VirtualAppliance"): |
| 15 | + self.name = name.replace(" ", "_") |
| 16 | + self.address_prefix = address_prefix |
| 17 | + self.next_hop_address = next_hop_address |
| 18 | + self.next_hop_type = next_hop_type |
| 19 | + |
| 20 | + |
| 21 | +class RouteTable: |
| 22 | + def __init__(self, name: str): |
| 23 | + self.name = name.replace(" ", "_") |
| 24 | + self.subnets: List[str] = [] |
| 25 | + self.routes: List[Route] = [] |
| 26 | + |
| 27 | + |
| 28 | +class AzureRouteTableRequest: |
| 29 | + """ |
| 30 | + Sample JSON structure to build |
| 31 | + { |
| 32 | + "route_tables": [ |
| 33 | + { |
| 34 | + "name": "myRouteTable1", |
| 35 | + "subnets": [ |
| 36 | + "subnetId1", |
| 37 | + "subnetId2" |
| 38 | + ], |
| 39 | + "routes": [ |
| 40 | + { |
| 41 | + "name": "myRoute1", |
| 42 | + "address_prefix": "10.0.1.0/28", |
| 43 | + "next_hop_type": "VirtualAppliance", |
| 44 | + "next_hop_address": "10.0.1.15" |
| 45 | + } |
| 46 | + ] |
| 47 | + }, |
| 48 | + { |
| 49 | + "name": "myRouteTable2", |
| 50 | + "subnets": [ |
| 51 | + "subnetId3", |
| 52 | + "subnetId4" |
| 53 | + ], |
| 54 | + "routes": [ |
| 55 | + { |
| 56 | + "name": "myRoute2", |
| 57 | + "address_prefix": "10.0.1.0/28", |
| 58 | + "next_hop_type": "VirtualAppliance", |
| 59 | + "next_hop_address": "10.0.1.15" |
| 60 | + } |
| 61 | + ] |
| 62 | + } |
| 63 | + ] |
| 64 | + } |
| 65 | + """ |
| 66 | + |
| 67 | + def __init__(self): |
| 68 | + self.route_tables: List[RouteTable] = [] |
| 69 | + |
| 70 | + def get_json(self): |
| 71 | + return json.dumps(self, default=lambda o: getattr(o, '__dict__', str(o))) |
| 72 | + |
| 73 | + def get_pretty_json(self): |
| 74 | + return json.dumps(self, default=lambda o: getattr(o, '__dict__', str(o)), indent=4) |
| 75 | + |
| 76 | + |
| 77 | +def _get_connector_endpoints(resource_name: str, resource_connectors: List[Connector]) -> List[str]: |
| 78 | + """ get back a list of connector endpoints from the sandbox details Connectors """ |
| 79 | + connector_endpoints = [] |
| 80 | + for connector in resource_connectors: |
| 81 | + if connector.Source == resource_name or connector.Target == resource_name: |
| 82 | + if connector.Source == resource_name: |
| 83 | + connector_endpoints.append(connector.Target) |
| 84 | + else: |
| 85 | + connector_endpoints.append(connector.Source) |
| 86 | + return connector_endpoints |
| 87 | + |
| 88 | + |
| 89 | +def _build_gateway_request_obj(api: CloudShellAPISession, res_id: str, target_resource_name: str): |
| 90 | + """ |
| 91 | + treats input resource name as central routing gateway when building route table |
| 92 | +
|
| 93 | + for sample topology: |
| 94 | + subnetA -> VA |
| 95 | + subnet B - > VA |
| 96 | + subnet C -> VA |
| 97 | +
|
| 98 | + create routes for: |
| 99 | + subnetA -> subnetB |
| 100 | + subnetA -> subnetC |
| 101 | + subnetB -> subnetA |
| 102 | + subnetB -> subnetC |
| 103 | + subnetC -> subnetA |
| 104 | + subnetC -> subnetB |
| 105 | + """ |
| 106 | + request_obj = AzureRouteTableRequest() |
| 107 | + |
| 108 | + # Network ID in VM Details matches subnet ID, so setting up dict for easy access |
| 109 | + resource_details = api.GetResourceDetails(target_resource_name) |
| 110 | + if not resource_details.VmDetails: |
| 111 | + raise SetAzureRoutesError(f"No VM Details found for {resource_details.Name}") |
| 112 | + network_id_map: Dict[str, VmDetailsNetworkInterface] = {x.NetworkId: x for x in resource_details.VmDetails.NetworkData} |
| 113 | + |
| 114 | + # pull all subnets in sandbox and find the ones connected to Virtual Appliance |
| 115 | + sb_details = api.GetReservationDetails(res_id, True).ReservationDescription |
| 116 | + connectors = sb_details.Connectors |
| 117 | + all_services = sb_details.Services |
| 118 | + connector_endpoints = _get_connector_endpoints(target_resource_name, connectors) |
| 119 | + subnet_services = [s for s in all_services if "subnet" in s.ServiceName.lower()] |
| 120 | + subnet_service_names = [s.Alias for s in subnet_services] |
| 121 | + subnet_endpoint_names = list(set(connector_endpoints).intersection(set(subnet_service_names))) |
| 122 | + connected_subnet_services = [s for s in subnet_services if s.Alias in subnet_endpoint_names] |
| 123 | + |
| 124 | + # iterate over connected services and add route to the other connected services |
| 125 | + for source_subnet in connected_subnet_services: |
| 126 | + subnet_id = [attr.Value for attr in source_subnet.Attributes if "subnet id" in attr.Name.lower()][0] |
| 127 | + network_additional_data = network_id_map[subnet_id].AdditionalData |
| 128 | + interface_ip = [x for x in network_additional_data if x.Name.lower() == "ip"][0].Value |
| 129 | + target_subnet_services = [s for s in connected_subnet_services if s.Alias != source_subnet.Alias] |
| 130 | + route_table_name = "{}-Source-RouteTable".format(source_subnet.Alias) |
| 131 | + curr_route_table = RouteTable(name=route_table_name) |
| 132 | + curr_route_table.subnets.append(subnet_id) |
| 133 | + request_obj.route_tables.append(curr_route_table) |
| 134 | + for target_service in target_subnet_services: |
| 135 | + route_name = f"Source__{source_subnet.Alias}-to-Target__{target_service.Alias}" |
| 136 | + target_cidr = [x for x in target_service.Attributes if "allocated cidr" in x.Name.lower()][0].Value |
| 137 | + if not target_cidr: |
| 138 | + raise Exception(f"Allocated CIDR not populated on service '{target_service.Alias}'") |
| 139 | + curr_route = Route(name=route_name, |
| 140 | + address_prefix=target_cidr, |
| 141 | + next_hop_address=interface_ip) |
| 142 | + curr_route_table.routes.append(curr_route) |
| 143 | + return request_obj |
| 144 | + |
| 145 | + |
| 146 | +def _send_route_table_request(api, res_id, azure_clp_resource, request_json): |
| 147 | + """ |
| 148 | + convenience wrapper for the command |
| 149 | + :param CloudShellAPISession api: |
| 150 | + :param str res_id: |
| 151 | + :param str azure_clp_resource: |
| 152 | + :param str request_json: |
| 153 | + """ |
| 154 | + return api.ExecuteCommand(reservationId=res_id, |
| 155 | + targetName=azure_clp_resource, |
| 156 | + targetType="Resource", |
| 157 | + commandName="CreateRouteTables", |
| 158 | + commandInputs=[InputNameValue("request", request_json)], |
| 159 | + printOutput=False).Output |
| 160 | + |
| 161 | + |
| 162 | +def set_virtual_appliance_routes(va_resource_name: str, clp_resource_name: str, api: CloudShellAPISession, res_id: str, |
| 163 | + reporter: SandboxReporter) -> str: |
| 164 | + try: |
| 165 | + azure_gateway_request_obj = _build_gateway_request_obj(api, res_id, va_resource_name) |
| 166 | + except Exception as e: |
| 167 | + exc_msg = "Issue building Azure routing request object: {}".format(str(e)) |
| 168 | + reporter.error(exc_msg) |
| 169 | + raise SetAzureRoutesError(exc_msg) |
| 170 | + |
| 171 | + request_json = azure_gateway_request_obj.get_json() |
| 172 | + pretty_json = azure_gateway_request_obj.get_pretty_json() |
| 173 | + |
| 174 | + reporter.warning("=== Sending Azure Route Tables Request ===") |
| 175 | + reporter.info(pretty_json) |
| 176 | + try: |
| 177 | + output = _send_route_table_request(api, res_id, clp_resource_name, request_json) |
| 178 | + except Exception as e: |
| 179 | + err_msg = f"Issue sending Azure route table request. {type(e).__name__}: {str(e)}" |
| 180 | + reporter.error(err_msg) |
| 181 | + raise SetAzureRoutesError(err_msg) |
| 182 | + return output |
0 commit comments