diff --git a/docs/webhook_events.md b/docs/webhook_events.md index 514762994..53964ed3b 100644 --- a/docs/webhook_events.md +++ b/docs/webhook_events.md @@ -645,6 +645,10 @@ Each event will be submitted via HTTP POST to the user provided URL. "allowed_aad_tenants": [ "00000000-0000-0000-0000-000000000000" ], + "network_config": { + "address_space": "10.0.0.0/8", + "subnet": "10.0.0.0/16" + }, "proxy_vm_sku": "Standard_B2s" } } @@ -752,6 +756,9 @@ Each event will be submitted via HTTP POST to the user provided URL. "extensions": { "$ref": "#/definitions/AzureVmExtensionConfig" }, + "network_config": { + "$ref": "#/definitions/NetworkConfig" + }, "proxy_vm_sku": { "default": "Standard_B2s", "title": "Proxy Vm Sku", @@ -791,6 +798,22 @@ Each event will be submitted via HTTP POST to the user provided URL. ], "title": "KeyvaultExtensionConfig", "type": "object" + }, + "NetworkConfig": { + "properties": { + "address_space": { + "default": "10.0.0.0/8", + "title": "Address Space", + "type": "string" + }, + "subnet": { + "default": "10.0.0.0/16", + "title": "Subnet", + "type": "string" + } + }, + "title": "NetworkConfig", + "type": "object" } }, "properties": { @@ -5804,6 +5827,9 @@ Each event will be submitted via HTTP POST to the user provided URL. "extensions": { "$ref": "#/definitions/AzureVmExtensionConfig" }, + "network_config": { + "$ref": "#/definitions/NetworkConfig" + }, "proxy_vm_sku": { "default": "Standard_B2s", "title": "Proxy Vm Sku", @@ -5895,6 +5921,22 @@ Each event will be submitted via HTTP POST to the user provided URL. "title": "KeyvaultExtensionConfig", "type": "object" }, + "NetworkConfig": { + "properties": { + "address_space": { + "default": "10.0.0.0/8", + "title": "Address Space", + "type": "string" + }, + "subnet": { + "default": "10.0.0.0/16", + "title": "Subnet", + "type": "string" + } + }, + "title": "NetworkConfig", + "type": "object" + }, "NoReproReport": { "properties": { "error": { diff --git a/src/api-service/__app__/onefuzzlib/azure/ip.py b/src/api-service/__app__/onefuzzlib/azure/ip.py index 2420669f4..9622c0378 100644 --- a/src/api-service/__app__/onefuzzlib/azure/ip.py +++ b/src/api-service/__app__/onefuzzlib/azure/ip.py @@ -13,10 +13,11 @@ from msrestazure.azure_exceptions import CloudError from msrestazure.tools import parse_resource_id from onefuzztypes.enums import ErrorCode from onefuzztypes.models import Error +from onefuzztypes.primitives import Region from .creds import get_base_resource_group +from .network import Network from .network_mgmt_client import get_network_client -from .subnet import create_virtual_network, get_subnet_id from .vmss import get_instance_id @@ -63,12 +64,12 @@ def delete_ip(resource_group: str, name: str) -> Any: return network_client.public_ip_addresses.begin_delete(resource_group, name) -def create_ip(resource_group: str, name: str, location: str) -> Any: - logging.info("creating ip for %s:%s in %s", resource_group, name, location) +def create_ip(resource_group: str, name: str, region: Region) -> Any: + logging.info("creating ip for %s:%s in %s", resource_group, name, region) network_client = get_network_client() params: Dict[str, Union[str, Dict[str, str]]] = { - "location": location, + "location": region, "public_ip_allocation_method": "Dynamic", } if "ONEFUZZ_OWNER" in os.environ: @@ -93,21 +94,24 @@ def delete_nic(resource_group: str, name: str) -> Optional[Any]: return network_client.network_interfaces.begin_delete(resource_group, name) -def create_public_nic(resource_group: str, name: str, location: str) -> Optional[Error]: - logging.info("creating nic for %s:%s in %s", resource_group, name, location) +def create_public_nic( + resource_group: str, name: str, region: Region +) -> Optional[Error]: + logging.info("creating nic for %s:%s in %s", resource_group, name, region) - network_client = get_network_client() - subnet_id = get_subnet_id(resource_group, location) - if not subnet_id: - return create_virtual_network(resource_group, location, location) + network = Network(region) + subnet_id = network.get_id() + if subnet_id is None: + network.create() + return None ip = get_ip(resource_group, name) if not ip: - create_ip(resource_group, name, location) + create_ip(resource_group, name, region) return None params = { - "location": location, + "location": region, "ip_configurations": [ { "name": "myIPConfig", @@ -119,6 +123,7 @@ def create_public_nic(resource_group: str, name: str, location: str) -> Optional if "ONEFUZZ_OWNER" in os.environ: params["tags"] = {"OWNER": os.environ["ONEFUZZ_OWNER"]} + network_client = get_network_client() try: network_client.network_interfaces.begin_create_or_update( resource_group, name, params diff --git a/src/api-service/__app__/onefuzzlib/azure/network.py b/src/api-service/__app__/onefuzzlib/azure/network.py index f9c7906c9..f2b0f4a17 100644 --- a/src/api-service/__app__/onefuzzlib/azure/network.py +++ b/src/api-service/__app__/onefuzzlib/azure/network.py @@ -4,43 +4,68 @@ # Licensed under the MIT License. import logging +import uuid from typing import Optional, Union from msrestazure.azure_exceptions import CloudError from onefuzztypes.enums import ErrorCode -from onefuzztypes.models import Error +from onefuzztypes.models import Error, NetworkConfig from onefuzztypes.primitives import Region +from ..config import InstanceConfig from .creds import get_base_resource_group -from .subnet import create_virtual_network, delete_subnet, get_subnet_id +from .subnet import create_virtual_network, get_subnet_id + +# This was generated randomly and should be preserved moving forwards +NETWORK_GUID_NAMESPACE = uuid.UUID("372977ad-b533-416a-b1b4-f770898e0b11") class Network: def __init__(self, region: Region): self.group = get_base_resource_group() self.region = region + self.network_config = InstanceConfig.fetch().network_config + + # Network names will be calculated from the address_space/subnet + # *except* if they are the original values. This allows backwards + # compatibility to existing configs if you don't change the network + # configs. + if ( + self.network_config.address_space + == NetworkConfig.__fields__["address_space"].default + and self.network_config.subnet == NetworkConfig.__fields__["subnet"].default + ): + self.name: str = self.region + else: + network_id = uuid.uuid5( + NETWORK_GUID_NAMESPACE, + "|".join( + [self.network_config.address_space, self.network_config.subnet] + ), + ) + self.name = f"{self.region}-{network_id}" def exists(self) -> bool: return self.get_id() is not None def get_id(self) -> Optional[str]: - return get_subnet_id(self.group, self.region) + return get_subnet_id(self.group, self.name, self.name) def create(self) -> Union[None, Error]: if not self.exists(): - result = create_virtual_network(self.group, self.region, self.region) + result = create_virtual_network( + self.group, self.name, self.region, self.network_config + ) if isinstance(result, CloudError): error = Error( code=ErrorCode.UNABLE_TO_CREATE_NETWORK, errors=[result.message] ) logging.error( - "network creation failed: %s- %s", + "network creation failed: %s:%s- %s", + self.name, self.region, error, ) return error return None - - def delete(self) -> None: - delete_subnet(self.group, self.region) diff --git a/src/api-service/__app__/onefuzzlib/azure/subnet.py b/src/api-service/__app__/onefuzzlib/azure/subnet.py index fdaf01192..5e4b5af2c 100644 --- a/src/api-service/__app__/onefuzzlib/azure/subnet.py +++ b/src/api-service/__app__/onefuzzlib/azure/subnet.py @@ -10,21 +10,23 @@ from typing import Any, Optional, Union, cast from azure.core.exceptions import ResourceNotFoundError from msrestazure.azure_exceptions import CloudError from onefuzztypes.enums import ErrorCode -from onefuzztypes.models import Error +from onefuzztypes.models import Error, NetworkConfig +from onefuzztypes.primitives import Region from .network_mgmt_client import get_network_client -def get_subnet_id(resource_group: str, name: str) -> Optional[str]: +def get_subnet_id(resource_group: str, name: str, subnet_name: str) -> Optional[str]: network_client = get_network_client() try: - subnet = network_client.subnets.get(resource_group, name, name) + subnet = network_client.subnets.get(resource_group, name, subnet_name) return cast(str, subnet.id) except (CloudError, ResourceNotFoundError): logging.info( - "subnet missing: resource group: %s name: %s", + "subnet missing: resource group:%s name:%s subnet_name:%s", resource_group, name, + subnet_name, ) return None @@ -43,20 +45,23 @@ def delete_subnet(resource_group: str, name: str) -> Union[None, CloudError, Any def create_virtual_network( - resource_group: str, name: str, location: str + resource_group: str, + name: str, + region: Region, + network_config: NetworkConfig, ) -> Optional[Error]: logging.info( - "creating subnet - resource group: %s name: %s location: %s", + "creating subnet - resource group:%s name:%s region:%s", resource_group, name, - location, + region, ) network_client = get_network_client() params = { - "location": location, - "address_space": {"address_prefixes": ["10.0.0.0/8"]}, - "subnets": [{"name": name, "address_prefix": "10.0.0.0/16"}], + "location": region, + "address_space": {"address_prefixes": [network_config.address_space]}, + "subnets": [{"name": name, "address_prefix": network_config.subnet}], } if "ONEFUZZ_OWNER" in os.environ: params["tags"] = {"OWNER": os.environ["ONEFUZZ_OWNER"]} diff --git a/src/api-service/__app__/onefuzzlib/azure/vmss.py b/src/api-service/__app__/onefuzzlib/azure/vmss.py index 14cd0f359..f9a1d4c31 100644 --- a/src/api-service/__app__/onefuzzlib/azure/vmss.py +++ b/src/api-service/__app__/onefuzzlib/azure/vmss.py @@ -399,11 +399,11 @@ def create_vmss( @cached(ttl=60) @retry_on_auth_failure() -def list_available_skus(location: str) -> List[str]: +def list_available_skus(region: Region) -> List[str]: compute_client = get_compute_client() skus: List[ResourceSku] = list( - compute_client.resource_skus.list(filter="location eq '%s'" % location) + compute_client.resource_skus.list(filter="location eq '%s'" % region) ) sku_names: List[str] = [] for sku in skus: @@ -411,7 +411,7 @@ def list_available_skus(location: str) -> List[str]: if sku.restrictions is not None: for restriction in sku.restrictions: if restriction.type == ResourceSkuRestrictionsType.location and ( - location.upper() in [v.upper() for v in restriction.values] + region.upper() in [v.upper() for v in restriction.values] ): available = False break diff --git a/src/api-service/__app__/onefuzzlib/request.py b/src/api-service/__app__/onefuzzlib/request.py index ebaad6d79..d25596812 100644 --- a/src/api-service/__app__/onefuzzlib/request.py +++ b/src/api-service/__app__/onefuzzlib/request.py @@ -92,8 +92,8 @@ def not_ok( ) -def redirect(location: str) -> HttpResponse: - return HttpResponse(status_code=302, headers={"Location": location}) +def redirect(url: str) -> HttpResponse: + return HttpResponse(status_code=302, headers={"Location": url}) def convert_error(err: ValidationError) -> Error: diff --git a/src/pytypes/onefuzztypes/models.py b/src/pytypes/onefuzztypes/models.py index 2dab04841..0e72090b8 100644 --- a/src/pytypes/onefuzztypes/models.py +++ b/src/pytypes/onefuzztypes/models.py @@ -794,6 +794,11 @@ class Task(BaseModel): user_info: Optional[UserInfo] +class NetworkConfig(BaseModel): + address_space: str = Field(default="10.0.0.0/8") + subnet: str = Field(default="10.0.0.0/16") + + class KeyvaultExtensionConfig(BaseModel): keyvault_name: str cert_name: str @@ -835,9 +840,8 @@ class InstanceConfig(BaseModel): allow_pool_management: bool = Field(default=True) allowed_aad_tenants: List[UUID] - + network_config: NetworkConfig = Field(default_factory=NetworkConfig) extensions: Optional[AzureVmExtensionConfig] - proxy_vm_sku: str = Field(default="Standard_B2s") def update(self, config: "InstanceConfig") -> None: