diff --git a/RELEASE_NOTES.md b/RELEASE_NOTES.md index 0cf17c8c0..7af767199 100644 --- a/RELEASE_NOTES.md +++ b/RELEASE_NOTES.md @@ -1,5 +1,8 @@ Release Notes ============= +## 1.7.24 +* Network Interface: Adds support for network interface creation. + ## 1.7.23 * Route server: Adds support for route server creation. * Storage Accounts: Fixes scope of role assignments. diff --git a/docs/content/api-overview/resources/network-interface.md b/docs/content/api-overview/resources/network-interface.md new file mode 100644 index 000000000..e48d8851a --- /dev/null +++ b/docs/content/api-overview/resources/network-interface.md @@ -0,0 +1,77 @@ +--- +title: "Network Interface" +chapter: false +weight: 5 +--- + +#### Overview +The `networkInterface` builder allows you to create network interfaces (NIC) so that Azure virtual machine (VM) can +communicate with internet, Azure, and on-premises resources. To learn more about routeServer, reference to +[Azure Docs](https://learn.microsoft.com/en-us/azure/virtual-network/virtual-network-network-interface?tabs=azure-portal) + +* NetworkInterface (`Microsoft.Network/networkInterfaces`) + +#### Builder Keywords + +| Applies To | Keyword | Purpose | +|-|------------------|-------------------------------------------------------------------------------------------------------| +| networkInterface | name | Name of the network interface resource | +| networkInterface | link_to_subnet | Link to existing subnet. If not provided, need to specify the subnet name and prefix for a new subnet | +| networkInterface | subnet_name | Sets the name of the vnet subnet for network interface | +| networkInterface | subnet_prefix | Sets the prefix of the vnet subnet for network interface | +| networkInterface | link_to_vnet | Link to existing vnet or to vnet managed by Farmer | +| networkInterface | add_static_ip | Use static ip for the network interface. If not provided, ip will be dynamically allocated | +| networkInterface | accelerated_networking_flag | The accelerated networking flag for the network interface. Default is false | +| networkInterface | ip_forwarding_flag | The ip forwarding flag for the network interface. Default is false | + +#### Example + +```fsharp +#r "nuget:Farmer" +open Farmer +open Farmer.Builders +open Farmer.Builders.NetworkInterface + +arm { + location Location.EastUS + + add_resources + [ + vnet { + name "test-vnet" + add_address_spaces [ "10.0.0.0/16" ] + } + networkInterface { + name "my-network-interface" + subnet_name "my-subnet" + subnet_prefix "10.0.100.0/24" + link_to_vnet (virtualNetworks.resourceId "test-vnet") + add_static_ip "10.0.100.10" + accelerated_networking_flag false + ip_forwarding_flag false + } + ] +} +``` + +#### Example using existing vnet and subnet with dynamic ip allocation + +```fsharp +#r "nuget:Farmer" +open Farmer +open Farmer.Builders +open Farmer.Builders.NetworkInterface + +arm { + location Location.EastUS + + add_resources + [ + networkInterface { + name "my-network-interface" + link_to_subnet "test-subnet" + link_to_vnet "test-vnet" + } + ] +} +``` \ No newline at end of file diff --git a/src/Farmer/Builders/Builders.NetworkInterface.fs b/src/Farmer/Builders/Builders.NetworkInterface.fs new file mode 100644 index 000000000..2d1b244b2 --- /dev/null +++ b/src/Farmer/Builders/Builders.NetworkInterface.fs @@ -0,0 +1,181 @@ +module Farmer.Builders.NetworkInterface + +open Farmer +open Farmer.Arm +open Farmer +open Farmer.Builders +open Farmer.Network +open Farmer.Arm.Network + +type NetworkInterfaceConfig = + { + Name: ResourceName + AcceleratedNetworkingflag: FeatureFlag option + IpForwarding: FeatureFlag option + IsPrimary: bool option + VirtualNetwork: LinkedResource option + SubnetName: string option + SubnetPrefix: IPAddressCidr option + LinkedSubnet: LinkedResource option + PrivateIpAddress: AllocationMethod + Tags: Map + } + + interface IBuilder with + member this.ResourceId = networkInterfaces.resourceId this.Name + + member this.BuildResources location = + [ + //vnet + let vnetId = + this.VirtualNetwork + |> Option.defaultWith (fun _ -> raiseFarmer "Must set 'vnet' for network interface") + + match this.LinkedSubnet with + | Some subnet -> + //ipConfig + let subnetIpConfigs = + [ + { + SubnetName = subnet.Name + LoadBalancerBackendAddressPools = [] + PublicIpAddress = None + PrivateIpAllocation = Some(this.PrivateIpAddress) + Primary = this.IsPrimary + } + ] + + //network interface + { + Name = this.Name + Location = location + EnableAcceleratedNetworking = + this.AcceleratedNetworkingflag |> Option.map (fun f -> f.AsBoolean) + EnableIpForwarding = this.IpForwarding |> Option.map (fun f -> f.AsBoolean) + IpConfigs = subnetIpConfigs + Primary = this.IsPrimary + VirtualNetwork = vnetId + NetworkSecurityGroup = None + Tags = this.Tags + } + + | None -> + match this.SubnetName, this.SubnetPrefix with + | Some subnetName, Some subnetPrefix -> + //subnet + { + Subnet.Name = ResourceName subnetName + Prefix = IPAddressCidr.format subnetPrefix + VirtualNetwork = Some(vnetId) + NetworkSecurityGroup = None + Delegations = [] + NatGateway = None + ServiceEndpoints = [] + AssociatedServiceEndpointPolicies = [] + PrivateEndpointNetworkPolicies = None + PrivateLinkServiceNetworkPolicies = None + } + + //ipConfig + let subnetIpConfigs = + [ + { + SubnetName = ResourceName subnetName + LoadBalancerBackendAddressPools = [] + PublicIpAddress = None + PrivateIpAllocation = Some(this.PrivateIpAddress) + Primary = this.IsPrimary + } + ] + + //network interface + { + Name = this.Name + Location = location + EnableAcceleratedNetworking = + this.AcceleratedNetworkingflag |> Option.map (fun f -> f.AsBoolean) + EnableIpForwarding = this.IpForwarding |> Option.map (fun f -> f.AsBoolean) + IpConfigs = subnetIpConfigs + Primary = this.IsPrimary + VirtualNetwork = vnetId + NetworkSecurityGroup = None + Tags = this.Tags + } + | _ -> + raiseFarmer + $"subnetName and subnetPrefix must be specified for a new subnet if no existing subnet provided." + ] + +type NetworkInterfaceBuilder() = + member _.Yield _ = + { + Name = ResourceName.Empty + AcceleratedNetworkingflag = None + IpForwarding = None + IsPrimary = None + VirtualNetwork = None + SubnetName = None + SubnetPrefix = None + LinkedSubnet = None + PrivateIpAddress = AllocationMethod.DynamicPrivateIp + Tags = Map.empty + } + + [] + member _.Name(state: NetworkInterfaceConfig, name: string) = { state with Name = ResourceName name } + + [] + member _.AcceleratedNetworkingflag(state: NetworkInterfaceConfig, flag: bool) = + { state with + AcceleratedNetworkingflag = Some(FeatureFlag.ofBool flag) + } + + [] + member _.IpForwarding(state: NetworkInterfaceConfig, flag: bool) = + { state with + IpForwarding = Some(FeatureFlag.ofBool flag) + } + + [] + member _.IsPrimary(state: NetworkInterfaceConfig, isPrimary: bool) = + { state with + IsPrimary = Some(isPrimary) + } + + // linked to managed vnet created by Farmer and linked by user + [] + member _.LinkToVNetId(state: NetworkInterfaceConfig, vnetId: ResourceId) = + { state with + VirtualNetwork = Some(Managed vnetId) + } + + // linked to external existing vnet + member _.LinkToVNetId(state: NetworkInterfaceConfig, vnetName: string) = + { state with + VirtualNetwork = Some(Unmanaged(virtualNetworks.resourceId (ResourceName vnetName))) + } + + // create subnet through Farmer. Need to specify subnet_name and subnet_prefix + [] + member _.SubnetName(state: NetworkInterfaceConfig, name) = { state with SubnetName = Some(name) } + + [] + member _.SubnetPrefix(state: NetworkInterfaceConfig, prefix) = + { state with + SubnetPrefix = Some(IPAddressCidr.parse prefix) + } + + // linked to external existing subnet + [] + member _.LinkToSubnet(state: NetworkInterfaceConfig, name: string) = + { state with + LinkedSubnet = Some(Unmanaged(subnets.resourceId (ResourceName name))) + } + + [] + member _.StaticIpAllocation(state: NetworkInterfaceConfig, addr) = + { state with + PrivateIpAddress = AllocationMethod.StaticPrivateIp(System.Net.IPAddress.Parse addr) + } + +let networkInterface = NetworkInterfaceBuilder() diff --git a/src/Farmer/Farmer.fsproj b/src/Farmer/Farmer.fsproj index 913b1f3f3..93f9a9c42 100644 --- a/src/Farmer/Farmer.fsproj +++ b/src/Farmer/Farmer.fsproj @@ -180,6 +180,7 @@ + diff --git a/src/Tests/Network.fs b/src/Tests/Network.fs index 85821cf72..df319c94a 100644 --- a/src/Tests/Network.fs +++ b/src/Tests/Network.fs @@ -6,6 +6,7 @@ open Newtonsoft.Json.Linq open Farmer open Farmer.Arm open Farmer.Builders +open Farmer.Builders.NetworkInterface open Farmer.Network open Microsoft.Rest @@ -1000,4 +1001,152 @@ let tests = let peerIp: string = JToken.op_Explicit bgpConnProps.["peerIp"] Expect.equal peerIp "10.0.1.85" "Incorrect peer ip for bgp connection" } + + test "Creates basic network interface with static ip" { + let deployment = + arm { + location Location.EastUS + + add_resources + [ + vnet { + name "test-vnet" + add_address_spaces [ "10.0.0.0/16" ] + } + networkInterface { + name "my-network-interface" + subnet_name "my-subnet" + subnet_prefix "10.0.100.0/24" + link_to_vnet (virtualNetworks.resourceId "test-vnet") + add_static_ip "10.0.100.10" + accelerated_networking_flag false + ip_forwarding_flag false + } + ] + } + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let templateStr = jobj.ToString() + + //validate vnet generated + let vnet = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks')]" + + Expect.isNotNull vnet "vnet should be generated" + + //validate subnet generated + let subnet = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/virtualNetworks/subnets')]" + + Expect.isNotNull subnet "subnet should be generated" + + let subnetName = subnet.["name"] + Expect.equal subnetName "test-vnet/my-subnet" "Incorrect default value for subnet name" + + let subnetProps = subnet.["properties"] + let addressPrefix: string = JToken.op_Explicit subnetProps.["addressPrefix"] + Expect.equal addressPrefix "10.0.100.0/24" "Incorrect addressPrefix for subnet" + + //validate network interface generated + let networkInterface = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/networkInterfaces')]" + + Expect.isNotNull networkInterface "network interface should be generated" + + let networkInterfaceName = networkInterface.["name"] + + Expect.equal + networkInterfaceName + "my-network-interface" + "Incorrect default value for network interface name" + + let networkInterfaceDependencies = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/networkInterfaces')].dependsOn" + :?> Newtonsoft.Json.Linq.JArray + + Expect.isNotNull networkInterfaceDependencies "Missing dependency for networkInterface" + Expect.hasLength networkInterfaceDependencies 1 "Incorrect number of dependencies for networkInterface" + + Expect.equal + (networkInterfaceDependencies.[0].ToString()) + "[resourceId(\u0027Microsoft.Network/virtualNetworks\u0027, \u0027test-vnet\u0027)]" + "Incorrect networkInterface dependencies" + + let networkInterfaceProps = networkInterface.["properties"] + + let enableAcceleratedNetworking: bool = + JToken.op_Explicit networkInterfaceProps.["enableAcceleratedNetworking"] + + Expect.equal enableAcceleratedNetworking false "Incorrect default value for enableAcceleratedNetworking" + + let enableIPForwarding: bool = + JToken.op_Explicit networkInterfaceProps.["enableIPForwarding"] + + Expect.equal enableIPForwarding false "Incorrect default value for enableIPForwarding" + + //validate ip config generated + let ipConfig = networkInterfaceProps.["ipConfigurations"].[0] + Expect.isNotNull ipConfig "network interface ip config should be generated" + + let ipConfigName = ipConfig.["name"] + Expect.equal ipConfigName "ipconfig1" "Incorrect default value for network interface ip config name" + + let ipConfigProps = ipConfig.["properties"] + + let privateIPAddress: string = JToken.op_Explicit ipConfigProps.["privateIPAddress"] + Expect.equal privateIPAddress "10.0.100.10" "Incorrect default value for privateIPAddress" + + let privateIPAllocationMethod: string = + JToken.op_Explicit ipConfigProps.["privateIPAllocationMethod"] + + Expect.equal privateIPAllocationMethod "Static" "Incorrect default value for privateIPAllocationMethod" + + let subnetId = ipConfigProps.SelectToken("subnet.id").ToString() + + Expect.equal + subnetId + "[resourceId(\u0027Microsoft.Network/virtualNetworks/subnets\u0027, \u0027test-vnet\u0027, \u0027my-subnet\u0027)]" + "Incorrect subnet id for ipConfig" + } + + test "Creates basic network interface with existing vnet subnet and dynamic ip" { + let deployment = + arm { + location Location.EastUS + + add_resources + [ + networkInterface { + name "my-network-interface" + link_to_subnet "test-subnet" + link_to_vnet "test-vnet" + } + ] + } + + let jobj = deployment.Template |> Writer.toJson |> JObject.Parse + let templateStr = jobj.ToString() + + let networkInterface = + jobj.SelectToken "resources[?(@.type=='Microsoft.Network/networkInterfaces')]" + + Expect.isNotNull networkInterface "network interface should be generated" + + let ipConfig = networkInterface.["properties"].["ipConfigurations"].[0] + Expect.isNotNull ipConfig "network interface ip config should be generated" + + let ipConfigProps = ipConfig.["properties"] + + let privateIPAllocationMethod: string = + JToken.op_Explicit ipConfigProps.["privateIPAllocationMethod"] + + Expect.equal privateIPAllocationMethod "Dynamic" "Incorrect default value for privateIPAllocationMethod" + + let subnetId = ipConfigProps.SelectToken("subnet.id").ToString() + + Expect.equal + subnetId + "[resourceId(\u0027Microsoft.Network/virtualNetworks/subnets\u0027, \u0027test-vnet\u0027, \u0027test-subnet\u0027)]" + "Incorrect subnet id for ipConfig" + } ]