.. _acme-topology: ********************************* Creating the ACME Topology Plugin ********************************* Recall that the ACME network will look like: .. image:: network_topology.png :alt: ACME Network Topology The actual code that builds the topology is the ACME model component's plugin which we specified as ``plugin.py`` in the ``MANIFEST`` file. Therefore, open the file ``acme/topology/plugin.py`` to get started. .. _acme-topo-abstract_plugin: Opening Our Plugin ================== Within every plugin file, FIREWHEEL automatically looks for a plugin class to define the plugin's execution. This class must meet two criteria. First, the class that gets declared must inherit from :py:class:`AbstractPlugin <firewheel.control.experiment_graph.AbstractPlugin>`. This guarantees that the :ref:`experiment-graph` instance is properly handled and located in the variable ``self.g``. Additionally, this inheritance also provides a logger in ``self.log`` to each plugin to facilitate easy debugging. Second, the plugin must have a ``run()`` method. This is the method that gets invoked by FIREWHEEL to kick off the plugin. The ``run()`` method can also take parameters, if needed, for the topology. As a note, only one such plugin class may be defined per plugin file (more would be ambiguous and therefore will cause FIREWHEEL to raise an error). With this context in mind, we will be editing the ``run()`` method first. Open the file ``acme/topology/plugin.py`` to get started. It should look something like this: .. code-block:: python from firewheel.control.experiment_graph import AbstractPlugin, Vertex class Plugin(AbstractPlugin): """acme.topology plugin documentation.""" def run(self): """Run method documentation.""" # TODO: Implement plugin actions here pass .. _acme-topo-run: Implementing the Plugin ======================= Because this topology is more complex than a few VMs, we recommend splitting up the functionality into separate methods. We can view the network graph as four distinct sections; the *Front*, *Building 1*, *Building 2*, and the *Data center*. Therefore, we can build the sections in individual methods and tie them all together in ``run()``. Set up ------ Before working on any particular part of the topology, we should first add a few necessary import statements. Add the following to the top of ``plugin.py``. .. code-block:: python from netaddr import IPNetwork from firewheel.control.experiment_graph import Vertex, AbstractPlugin from base_objects import Switch from vyos.helium118 import Helium118 from linux.ubuntu2204 import Ubuntu2204Server, Ubuntu2204Desktop These imports provide the necessary graph objects needed to create the topology. Additionally, we will use :py:mod:`netaddr` to facilitate assigning IP addresses easier. Let's initialize a few instance variables which can be used when creating the topology. Using :py:mod:`netaddr` we will create an external-facing network (e.g. "Internet" facing). We aren't going to put anything external to the ACME enterprise in this tutorial, but you could add to this topology by providing external services. The external network is going to be ``1.0.0.0/24``. Next, we need an internal network for routing between the distinct sections of the enterprise. We're going to use ``10.0.0.0/8`` and the subsequently break up the network into subnet blocks each with 255 IP addresses. While users can define IP addresses with simple Python strings, for complex topologies we recommend using the :py:class:`netaddr.IPNetwork` and :py:class:`netaddr.IPAddress` classes to specify various network spaces. :py:class:`netaddr.IPNetwork` provides a generator (:py:meth:`iter_hosts() <netaddr.IPNetwork.iter_hosts>`) that allows you to walk through the entire IP space. The generator provides :py:class:`IPAddress <netaddr.IPAddress>` objects. Therefore, we can use standard python syntax to get the next available :py:class:`IPAddress <netaddr.IPAddress>` (e.g. ``next(network_iter)``). We will use this generator method throughout the creation of the ACME topology. Here is our initial ``run()`` method. .. code-block:: python def run(self): """Run method documentation.""" # Create an external-facing network and an iterator for that network. # The iterator will provide the next available netaddr.IPAddress for the given # network. self.external_network = IPNetwork("1.0.0.0/24") external_network_iter = self.external_network.iter_hosts() # Create an internal facing network internal_networks = IPNetwork("10.0.0.0/8") # Break the internal network into various subnets # https://netaddr.readthedocs.io/en/latest/tutorial_01.html#supernets-and-subnets self.internal_subnets = internal_networks.subnet(24) Building the Front ------------------ Now we are ready to build out the first part of the topology which we will call the "front". The "front" of the enterprise consists of the gateway router and the firewall. We will create a new method called ``build_front()``. For this method, we can pass in the external IP network for the gateway. .. code-block:: python def build_front(self, ext_ip): pass Now we need to create the gateway router. We create a vertex by instantiating a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` object and passing it the graph (``self.g``) as well as the name of the vertex. The ``plugin.py`` template has already imported :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` for you from :py:mod:`firewheel.control.experiment_graph`. Once we have created a :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>`, we can decorate it as a specific object type. In this case, we want the gateway to be a VyOS router using the Helium 1.1.8 release. We have imported the the :py:class:`Helium118 <vyos.helium118.Helium118>` object already (from the :ref:`vyos.helium118_mc` MC) so we can use it to decorate our gateway. .. note:: Vertices can be given default image types in the case where a specific image class isn't known at topology creation. We could have decorated this :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` with :py:class:`GenericRouter <generic_vm_objects.GenericRouter>` (from the :ref:`generic_vm_objects_mc` MC) and it would have subsequently been decorated as :py:class:`Helium118 <vyos.helium118.Helium118>` due to the defaults set in the :ref:`minimega.resolve_vm_images_mc` model component. See :ref:`minimega.resolve_vm_images_mc` for more details. .. code-block:: python def build_front(self, ext_ip): # Build the gateway gateway = Vertex(self.g, "gateway.acme.com") gateway.decorate(Helium118) In FIREWHEEL :py:class:`Switches <base_objects.Switch>` are essentially virtual network bridges which help connect two VMs. Users *can* make a Switch into a VM if some specific switching technique is being evaluated, but typically, they will just be instantiated as an Open vSwitch bridge that is transparent to the VMs within the experiment. A :py:class:`Switch <base_objects.Switch>` is created in a way that is very similar to the routers. The only difference is that the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` is decorated with :py:class:`Switch <base_objects.Switch>`, which can be imported from :ref:`base_objects_mc`. We can now create our "external" switch. .. code-block:: python # Create the external switch ext_switch = Vertex(self.g, name="ACME-EXTERNAL") ext_switch.decorate(Switch) Now that we have the :py:class:`Switch <base_objects.Switch>` and the gateway, we can connect them together. We will use the IP address which was passed into the method. .. code-block:: python # Connect the gateway to the external switch gateway.connect( ext_switch, # The "Internet" facing Switch ext_ip, # The external IP address for the gateway (e.g. 1.0.0.1) self.external_network.netmask # The external subnet mask (e.g. 255.255.255.0) ) We then do the same thing to create the firewall and the switch to connect the firewall to the gateway. .. code-block:: python # Build a switch to connect the gateway and firewall gateway_firewall_switch = Vertex(self.g, name="GW-FW") gateway_firewall_switch.decorate(Switch) # Build the firewall firewall = Vertex(self.g, "firewall.acme.com") firewall.decorate(Helium118) We will want to create a network to generate the IP address for the gateway/firewall connection. To do so, we can grab the next available subnet from our ``self.internal_subnets`` generator. .. code-block:: python # Create a network and a generator for the network between # the gateway and firewall. gateway_firewall_network = next(self.internal_subnets) gateway_firewall_network_iter = gateway_firewall_network.iter_hosts() Since this is a network local to the enterprise, let's have the routers use the `OSPF <https://en.wikipedia.org/wiki/Open_Shortest_Path_First>`__ routing protocol. When using OSPF, you can call the :py:meth:`ospf_connect() <generic_vm_objects.GenericRouter.ospf_connect>` method on the router. The :py:meth:`ospf_connect() <generic_vm_objects.GenericRouter.ospf_connect>` method requires that you specify the switch to connect the router to, the IP address for the router's interface, and the netmask of the IP. This method takes care of all the relevant details to make sure that OSPF works on the newly created network interface. .. code-block:: python # Connect the gateway and the firewall to their respective switches # We will use ``ospf_connect`` to ensure that the OSPF routes are propagated # correctly (as we want to use OSPF as routing protocol inside of the ACME network). gateway.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) firewall.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) Finally, we return the firewall :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` so that other parts of the topology can connect to them as well. .. code-block:: python return firewall The full ``build_front()`` method is: .. code-block:: python def build_front(self, ext_ip): """Build the ACME infrastructure that is Internet-facing. This method will create the following topology:: switch -- gateway -- switch -- firewall (ACME-EXTERNAL) (GW-FW) Args: ext_ip (netaddr.IPAddress): The external IP address for the gateway (e.g. its Internet facing IP address). Returns: vyos.Helium118: The Firewall object. """ # Build the gateway gateway = Vertex(self.g, "gateway.acme.com") gateway.decorate(Helium118) # Create the external switch ext_switch = Vertex(self.g, name="ACME-EXTERNAL") ext_switch.decorate(Switch) # Connect the gateway to the external switch gateway.connect( ext_switch, # The "Internet" facing Switch ext_ip, # The external IP address for the gateway (e.g. 1.0.0.1) self.external_network.netmask # The external subnet mask (e.g. 255.255.255.0) ) # Build a switch to connect the gateway and firewall gateway_firewall_switch = Vertex(self.g, name="GW-FW") gateway_firewall_switch.decorate(Switch) # Build the firewall firewall = Vertex(self.g, "firewall.acme.com") firewall.decorate(Helium118) # Create a network and a generator for the network between # the gateway and firewall. gateway_firewall_network = next(self.internal_subnets) gateway_firewall_network_iter = gateway_firewall_network.iter_hosts() # Connect the gateway and the firewall to their respective switches # We will use ``ospf_connect`` to ensure that the OSPF routes are propagated # correctly (as we want to use OSPF as routing protocol inside of the ACME network). gateway.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) firewall.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) return firewall Updating ``run()`` ------------------ Now that we have a method which will create the first part of our ACME network, we can update our ``run()`` method to call ``build_front()``. Recall that we needed to pass in the an external IP address. .. code-block:: python def run(self): ... # Create the gateway and firewall firewall = self.build_front(next(external_network_iter)) Now, we can create a new :py:class:`Switch <base_objects.Switch>` and a new subnet which will be used to connect both buildings to the firewall. .. code-block:: python :emphasize-lines: 6-13 def run(self): ... # Create the gateway and firewall firewall = self.build_front(next(external_network_iter)) # Create an internal switch internal_switch = Vertex(self.g, name="ACME-INTERNAL") internal_switch.decorate(Switch) # Grab a subnet to use for connections to the internal switch internal_switch_network = next(self.internal_subnets) # Create a generator for the network internal_switch_network_iter = internal_switch_network.iter_hosts() Once we have set up internal network and Switch, we can connect the firewall to that switch. .. code-block:: python :emphasize-lines: 6-11 def run(self): ... # Create a generator for the network internal_switch_network_iter = internal_switch_network.iter_hosts() # Connect the firewall to the internal switch firewall.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) We are now ready to create a method to create a building. Implementing ``build_building()`` --------------------------------- With the front done, it's now time to create an ACME building. Since we want multiple buildings and the buildings themselves are very similar, we can make a single ``build_building()`` method that gets called multiple times from ``run()``. We will want to pass in several method parameters to ``build_building()``: * ``name`` - The name of the building (e.g. ``"building1"``). * ``network`` - The :py:class:`netaddr.IPNetwork` subnet for the particular building. This will be used to connect all hosts within the building. * ``num_hosts`` - The number of hosts the building should have. .. code-block:: python def build_building(self, name, network, num_hosts=1): pass Every building needs a router in order to connect to the rest of the ACME enterprise. We use the ``name`` that was provided to the function as the name for the router. Like the routers that were created in ``build_front()``, we decorate the :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` as a :py:class:`Helium118 <vyos.helium118.Helium118>` router. .. code-block:: python def build_building(self, name, network, num_hosts=1): """Create the building router and hosts. This is a single router with all of the hosts. Assuming that the building is called "building1" the topology will look like:: switch ---- building1 ----- switch ------ hosts (ACME-INTERNAL) (building1-switch) Args: name (str): The name of the building. network (netaddr.IPNetwork): The subnet for the building. num_hosts (int): The number of hosts the building should have. Returns: vyos.Helium118: The building router. """ # Create the VyOS router which will connect the building to the ACME network. building = Vertex(self.g, name=f"{name}.acme.com") building.decorate(Helium118) Now, we can create a new :py:class:`Switch <base_objects.Switch>` and a new :py:class:`IPAddress <netaddr.IPAddress>` generator which will be used to connect the building router to all building hosts. .. code-block:: python # Create the building-specific switch building_sw = Vertex(self.g, name=f"{name}-switch") building_sw.decorate(Switch) # Create a generator for the building's network building_network_iter = network.iter_hosts() We can now connect the building router to the building :py:class:`Switch <base_objects.Switch>`. In this case, because none of the hosts use OSPF to communicate, we can directly connect the router to the switch using the :py:meth:`connect() <base_objects.VMEndpoint.connect>` (rather than using :py:meth:`ospf_connect() <generic_vm_objects.GenericRouter.ospf_connect>`). However, because the ACME internal network uses OSPF to communicate, we will want to ensure that the building can be discovered by the rest of the ACME network. Therefore, we use the :py:meth:`redistribute_ospf_connected() <generic_vm_objects.GenericRouter.redistribute_ospf_connected>` method to redistribute (i.e., advertise) networks that it is directly connected to (i.e., the building's network). This will make the hosts routable (and discoverable) throughout the rest of the ACME enterprise. .. code-block:: python # Create the building-specific switch building_sw = Vertex(self.g, name=f"{name}-switch") building_sw.decorate(Switch) # Create a generator for the building's network building_network_iter = network.iter_hosts() # Connect the building to the building Switch building.connect(building_sw, next(building_network_iter), network.netmask) # This redistributes routes for directly connected subnets to OSPF peers. # That is, enables these peers to be discoverable by the rest of the OSPF # routing infrastructure. building.redistribute_ospf_connected() The building has a parameter which defines the number of end hosts that require access to the enterprise network (i.e. ``num_hosts``). We can use a loop to create the requisite number of hosts. In this case, we want to decorate our vertices with :py:class:`Ubuntu2204Desktop <linux.ubuntu2204.Ubuntu2204Desktop>`, which we imported from the :ref:`linux.ubuntu2204_mc`. Once each host is created, we can add it to the building :py:class:`Switch <base_objects.Switch>`. .. code-block:: python # Create the correct number of hosts for i in range(num_hosts): # Create a new host which is a Ubuntu Desktop host = Vertex( self.g, name=f"{name}-host-{i}.acme.com", # e.g. "building1-host-1.acme.com" ) host.decorate(Ubuntu2204Desktop) # Connect the host to the building's switch host.connect( building_sw, # The building switch next(building_network_iter), # The next available building IP address network.netmask, # The building's subnet mask ) Now that all the hosts are connected we can return the building router to connect it to the ``ACME-INTERNAL`` :py:class:`Switch <base_objects.Switch>`. .. code-block:: python return building The full ``build_front()`` method is: .. code-block:: python def build_building(self, name, network, num_hosts=1): """Create the building router and hosts. This is a single router with all of the hosts. Assuming that the building is called "building1" the topology will look like:: switch ---- building1 ----- switch ------ hosts (ACME-INTERNAL) (building1-switch) Args: name (str): The name of the building. network (netaddr.IPNetwork): The subnet for the building. num_hosts (int): The number of hosts the building should have. Returns: vyos.Helium118: The building router. """ # Create the VyOS router which will connect the building to the ACME network. building = Vertex(self.g, name=f"{name}.acme.com") building.decorate(Helium118) # Create the building-specific switch building_sw = Vertex(self.g, name=f"{name}-switch") building_sw.decorate(Switch) # Create a generator for the building's network building_network_iter = network.iter_hosts() # Connect the building to the building Switch building.connect(building_sw, next(building_network_iter), network.netmask) # This redistribute routes for directly connected subnets to OSPF peers. # That is, enables these peers to be discoverable by the rest of the OSPF # routing infrastructure. building.redistribute_ospf_connected() # Create the correct number of hosts for i in range(num_hosts): # Create a new host which is a Ubuntu Desktop host = Vertex( self.g, name=f"{name}-host-{i}.acme.com", # e.g. "building1-host-1.acme.com" ) host.decorate(Ubuntu2204Desktop) # Connect the host to the building's switch host.connect( building_sw, # The building switch next(building_network_iter), # The next available building IP address network.netmask, # The building's subnet mask ) return building Adding buildings to ``run()`` ----------------------------- Recall that we had just connected the firewall to the internal switch in the ``run()`` method. .. code-block:: python def run(self): ... # Connect the Firewall to the internal switch firewall.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) We already have all the pieces in place to create our two buildings and connect them to the same internal switch. .. code-block:: python def run(self): ... building_1 = self.build_building( "building1", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the first building router to the internal switch. building_1.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Create our second building building_2 = self.build_building( "building2", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the second building router to the internal switch. building_2.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) Now that our buildings are ready, we can begin constructing the data center. This will also be a separate method. Implementing ``build_datacenter()`` ----------------------------------- The data center hosts the servers for our fictional ACME enterprise. The slight difference for this method is that the data center is housed in Building 2 and therefore uses the Building 2 router to connect into the rest of the internal network. This is set by passing in the Building 2 router as the first parameter of the ``build_datacenter()`` method. Additionally, the data center will need two different networks, one for connecting the DC to the building and a second for connecting the DC to its various servers. .. code-block:: python def build_datacenter(self, building, uplink_network, dc_network): pass The ``build_datacenter()`` method will look similar to creating a building. We will create a DC :py:class:`Vertex <firewheel.control.experiment_graph.Vertex>` and decorate it as a a :py:class:`Helium118 <vyos.helium118.Helium118>` router. We will then create a :py:class:`Switch <base_objects.Switch>` and an :py:class:`IPAddress <netaddr.IPAddress>` generator from the ``uplink_network`` and connect the DC router and the building router to the switch using the :py:meth:`ospf_connect() <generic_vm_objects.GenericRouter.ospf_connect>` method. Next, we will create the internal DC switch and a :py:func:`for loop <for>` to create all of our servers. In this case, the servers will be decorated as a :py:class:`Ubuntu2204Server <linux.ubuntu2204.Ubuntu2204Server>` (rather than a :py:class:`Ubuntu2204Desktop <linux.ubuntu2204.Ubuntu2204Desktop>`). The full method looks like: .. code-block:: python def build_datacenter(self, building, uplink_network, dc_network): """Create the data center. This is a single router with all of the servers:: building2 ------ switch ------ datacenter ------ switch ------ servers (building2-DC-switch) (DC-switch) Args: building (vyos.Helium118): The Building router which contains the data center. uplink_network (netaddr.IPNetwork): The network to connect the DC to the building. dc_network (netaddr.IPNetwork): The network for the data center. """ # Create a switch to connect the DC with the building building_dc_sw = Vertex(self.g, name=f"{building.name}-DC-switch") building_dc_sw.decorate(Switch) # Create the datacenter router datacenter = Vertex(self.g, name="datacenter.acme.com") datacenter.decorate(Helium118) # Create a generator for the building's network uplink_network_iter = uplink_network.iter_hosts() # Connect the building to the building-DC-switch building.ospf_connect( building_dc_sw, next(uplink_network_iter), uplink_network.netmask ) # Connect the datacenter to the building-DC-switch datacenter.ospf_connect( building_dc_sw, next(uplink_network_iter), uplink_network.netmask ) # Make the datacenter internal switch and connect datacenter_sw = Vertex(self.g, name="DC-switch") datacenter_sw.decorate(Switch) # Create a generator for the DC's network dc_network_iter = dc_network.iter_hosts() # Connect the DC to the internal switch datacenter.connect(datacenter_sw, next(dc_network_iter), dc_network.netmask) # This redistribute routes for directly connected subnets to OSPF peers. # That is, enables these peers to be discoverable by the rest of the OSPF # routing infrastructure. datacenter.redistribute_ospf_connected() # Make servers for i in range(3): # Create a new Ubuntu server and add connect it to the DC network switch server = Vertex(self.g, name=f"datacenter-{i}.acme.com") server.decorate(Ubuntu2204Server) server.connect(datacenter_sw, next(dc_network_iter), dc_network.netmask) Calling ``build_datacenter()`` ------------------------------ Lastly, we need to call the ``build_datacenter()`` from our run function. You can simply add the following lines to the bottom of ``run()``. .. code-block:: python # Build our data center self.build_datacenter( building_2, # The building Vertex next( self.internal_subnets ), # Add a network to connect the DC to the building next(self.internal_subnets), # Add a network which is internal to the DC ) Here is what the full ``run()`` function should look like: .. code-block:: python def run(self): """Run method documentation.""" # Create an external-facing network and an iterator for that network. # The iterator will provide the next available netaddr.IPAddress for the given # network. self.external_network = IPNetwork("1.0.0.0/24") external_network_iter = self.external_network.iter_hosts() # Create an internal facing network internal_networks = IPNetwork("10.0.0.0/8") # Break the internal network into various subnets # https://netaddr.readthedocs.io/en/latest/tutorial_01.html#supernets-and-subnets self.internal_subnets = internal_networks.subnet(24) # Create the gateway and firewall firewall = self.build_front(next(external_network_iter)) # Create an internal switch internal_switch = Vertex(self.g, name="ACME-INTERNAL") internal_switch.decorate(Switch) # Grab a subnet to use for connections to the internal switch internal_switch_network = next(self.internal_subnets) # Create a generator for the network internal_switch_network_iter = internal_switch_network.iter_hosts() # Connect the Firewall to the internal switch firewall.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Create our first building building_1 = self.build_building( "building1", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the first building router to the internal switch. building_1.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Create our second building building_2 = self.build_building( "building2", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the second building router to the internal switch. building_2.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Build our data center self.build_datacenter( building_2, # The building Vertex next( self.internal_subnets ), # Add a network to connect the DC to the building next(self.internal_subnets), # Add a network which is internal to the DC ) Putting it All Together ----------------------- At this point, you can save your file and close the editor. It is now time to verify that your topology works as expected. For reference, the full ``plugin.py`` file should look something like this: .. code-block:: python from netaddr import IPNetwork from firewheel.control.experiment_graph import Vertex, AbstractPlugin from base_objects import Switch from vyos.helium118 import Helium118 from linux.ubuntu2204 import Ubuntu2204Server, Ubuntu2204Desktop class Plugin(AbstractPlugin): """acme.topology plugin documentation.""" def run(self): """Run method documentation.""" # Create an external-facing network and an iterator for that network. # The iterator will provide the next available netaddr.IPAddress for the given # network. self.external_network = IPNetwork("1.0.0.0/24") external_network_iter = self.external_network.iter_hosts() # Create an internal facing network internal_networks = IPNetwork("10.0.0.0/8") # Break the internal network into various subnets # https://netaddr.readthedocs.io/en/latest/tutorial_01.html#supernets-and-subnets self.internal_subnets = internal_networks.subnet(24) # Create the gateway and firewall firewall = self.build_front(next(external_network_iter)) # Create an internal switch internal_switch = Vertex(self.g, name="ACME-INTERNAL") internal_switch.decorate(Switch) # Grab a subnet to use for connections to the internal switch internal_switch_network = next(self.internal_subnets) # Create a generator for the network internal_switch_network_iter = internal_switch_network.iter_hosts() # Connect the Firewall to the internal switch firewall.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Create our first building building_1 = self.build_building( "building1", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the first building router to the internal switch. building_1.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Create our second building building_2 = self.build_building( "building2", # The name of the building next(self.internal_subnets), # The building network num_hosts=3, # The number of hosts for the building ) # Connect the second building router to the internal switch. building_2.ospf_connect( internal_switch, next(internal_switch_network_iter), internal_switch_network.netmask, ) # Build our data center self.build_datacenter( building_2, # The building Vertex next( self.internal_subnets ), # Add a network to connect the DC to the building next(self.internal_subnets), # Add a network which is internal to the DC ) def build_front(self, ext_ip): """Build the ACME infrastructure that is Internet-facing. This method will create the following topology:: switch -- gateway -- switch -- firewall (ACME-EXTERNAL) (GW-FW) Args: ext_ip (netaddr.IPAddress): The external IP address for the gateway (e.g. its Internet facing IP address). Returns: vyos.Helium118: The Firewall object. """ # Build the gateway gateway = Vertex(self.g, "gateway.acme.com") gateway.decorate(Helium118) # Create the external switch ext_switch = Vertex(self.g, name="ACME-EXTERNAL") ext_switch.decorate(Switch) # Connect the gateway to the external switch gateway.connect( ext_switch, # The "Internet" facing Switch ext_ip, # The external IP address for the gateway (e.g. 1.0.0.1) self.external_network.netmask, # The external subnet mask (e.g. 255.255.255.0) ) # Build a switch to connect the gateway and firewall gateway_firewall_switch = Vertex(self.g, name="GW-FW") gateway_firewall_switch.decorate(Switch) # Build the firewall firewall = Vertex(self.g, "firewall.acme.com") firewall.decorate(Helium118) # Create a network and a generator for the network between # the gateway and firewall. gateway_firewall_network = next(self.internal_subnets) gateway_firewall_network_iter = gateway_firewall_network.iter_hosts() # Connect the gateway and the firewall to their respective switches # We will use ``ospf_connect`` to ensure that the OSPF routes are propagated # correctly (as we want to use OSPF as routing protocol inside of the ACME network). gateway.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) firewall.ospf_connect( gateway_firewall_switch, next(gateway_firewall_network_iter), gateway_firewall_network.netmask, ) return firewall def build_building(self, name, network, num_hosts=1): """Create the building router and hosts. This is a single router with all of the hosts. Assuming that the building is called "building1" the topology will look like:: switch ---- building1 ----- switch ------ hosts (ACME-INTERNAL) (building1-switch) Args: name (str): The name of the building. network (netaddr.IPNetwork): The subnet for the building. num_hosts (int): The number of hosts the building should have. Returns: vyos.Helium118: The building router. """ # Create the VyOS router which will connect the building to the ACME network. building = Vertex(self.g, name=f"{name}.acme.com") building.decorate(Helium118) # Create the building-specific switch building_sw = Vertex(self.g, name=f"{name}-switch") building_sw.decorate(Switch) # Create a generator for the building's network building_network_iter = network.iter_hosts() # Connect the building to the building Switch building.connect(building_sw, next(building_network_iter), network.netmask) # This redistribute routes for directly connected subnets to OSPF peers. # That is, enables these peers to be discoverable by the rest of the OSPF # routing infrastructure. building.redistribute_ospf_connected() # Create the correct number of hosts for i in range(num_hosts): # Create a new host which is a Ubuntu Desktop host = Vertex( self.g, name=f"{name}-host-{i}.acme.com", # e.g. "building1-host-1.acme.com" ) host.decorate(Ubuntu2204Desktop) # Connect the host to the building's switch host.connect( building_sw, # The building switch next(building_network_iter), # The next available building IP address network.netmask, # The building's subnet mask ) return building def build_datacenter(self, building, uplink_network, dc_network): """Create the data center. This is a single router with all of the servers:: building2 ------ switch ------ datacenter ------ switch ------ servers (building2-DC-switch) (DC-switch) Args: building (vyos.Helium118): The Building router which contains the data center. uplink_network (netaddr.IPNetwork): The network to connect the DC to the building. dc_network (netaddr.IPNetwork): The network for the data center. """ # Create a switch to connect the DC with the building building_dc_sw = Vertex(self.g, name=f"{building.name}-DC-switch") building_dc_sw.decorate(Switch) # Create the datacenter router datacenter = Vertex(self.g, name="datacenter.acme.com") datacenter.decorate(Helium118) # Create a generator for the building's network uplink_network_iter = uplink_network.iter_hosts() # Connect the building to the building-DC-switch building.ospf_connect( building_dc_sw, next(uplink_network_iter), uplink_network.netmask ) # Connect the datacenter to the building-DC-switch datacenter.ospf_connect( building_dc_sw, next(uplink_network_iter), uplink_network.netmask ) # Make the datacenter internal switch and connect datacenter_sw = Vertex(self.g, name="DC-switch") datacenter_sw.decorate(Switch) # Create a generator for the DC's network dc_network_iter = dc_network.iter_hosts() # Connect the DC to the internal switch datacenter.connect(datacenter_sw, next(dc_network_iter), dc_network.netmask) # This redistribute routes for directly connected subnets to OSPF peers. # That is, enables these peers to be discoverable by the rest of the OSPF # routing infrastructure. datacenter.redistribute_ospf_connected() # Make servers for i in range(3): # Create a new Ubuntu server and add connect it to the DC network switch server = Vertex(self.g, name=f"datacenter-{i}.acme.com") server.decorate(Ubuntu2204Server) server.connect(datacenter_sw, next(dc_network_iter), dc_network.netmask)