Example #1
0
def first_fit(ordering=None):
    """ First-Fit heuristic (hosts each VM in the first server with enough resources).
    For pragmatism purposes, we can define the way VMs are ordered. By default, we can
    select "Increasing" or "Decreasing". If we don't pass the ordering parameter, the function
    will define a placement for VMs based on the ordering provided by the input file.

    Parameters
    ==========
    ordering : String
        Ordering of VMs to be used by the First-Fit heuristic
    """

    # Ordering VMs before selecting the VM placement
    vms = vms_ordering(ordering=ordering)

    for vm in vms:
        for server in Server.all():
            if server.has_capacity_to_host(vm):

                server.virtual_machines.append(vm)

                server.cpu_demand += vm.cpu_demand
                server.memory_demand += vm.memory_demand
                server.disk_demand += vm.disk_demand

                vm.server = server

                break
    def collect_metrics(self):
        """ Stores relevant events that occur during the simulation.
        """

        # Collecting Server metrics
        servers = []
        for server in Server.all():
            server_data = { 'server': server, 'occupation_rate': server.occupation_rate(),
                'cpu_capacity': server.cpu_capacity, 'memory_capacity': server.memory_capacity,
                'disk_capacity': server.disk_capacity, 'cpu_demand': server.cpu_demand,
                'memory_demand': server.memory_demand, 'disk_demand': server.disk_demand,
                'virtual_machines': server.virtual_machines, 'updated': server.updated,
                'update_step': server.update_step }

            servers.append(server_data)


        # Collecting VirtualMachine metrics
        virtual_machines = []
        for vm in VirtualMachine.all():
            vm_data = { 'cpu_demand': vm.cpu_demand, 'memory_demand': vm.memory_demand,
            'disk_demand': vm.disk_demand, 'server_update_status': vm.server.updated,
            'migrations': vm.migrations }

            virtual_machines.append(vm_data)


        # Creating the structure to accommodate simulation metrics
        self.metrics.append({'maintenance_step': self.maintenance_step, 'simulation_step': self.env.now,
            'servers': servers, 'virtual_machines': virtual_machines})
Example #3
0
def greedy_least_batch():
    """ Maintenance strategy proposed in [1]. It is designed to
    minimize the number of maintenance steps necessary to update
    the data center.

    References
    ==========
    [1] Zheng, Zeyu, et al. "Least maintenance batch scheduling in cloud
    data center networks." IEEE communications letters 18.6 (2014): 901-904.
    """

    # Patching servers nonupdated servers that are not hosting VMs
    servers_to_patch = Server.ready_to_patch()
    if len(servers_to_patch) > 0:
        servers_patch_duration = []

        for server in servers_to_patch:
            patch_duration = server.update()
            servers_patch_duration.append(patch_duration)

        # As servers are updated simultaneously, we don't need to call the function
        # that quantifies the server maintenance duration for each server being patched
        yield SimulationEnvironment.first().env.timeout(max(servers_patch_duration))


    # Migrating VMs
    else:
        servers_being_emptied = []

        # Sorts the servers to empty based on their occupation rate (ascending)
        servers_to_empty = sorted(Server.nonupdated(), key=lambda sv: sv.occupation_rate())

        for server in servers_to_empty:
            # We consider as candidate hosts for the VMs every server
            # not being emptied in the current iteration
            candidate_servers = [cand_server for cand_server in Server.all()
                if cand_server not in servers_being_emptied and cand_server != server]

            vms = [vm for vm in server.virtual_machines]

            if Server.can_host_vms(candidate_servers, vms):
                for _ in range(len(server.virtual_machines)):
                    vm = vms.pop(0)

                    # Sorting servers by update status (updated ones first) and demand (more occupied ones first)
                    candidate_servers = sorted(candidate_servers, key=lambda cand_server:
                        (-cand_server.updated, -cand_server.occupation_rate()))

                    # Using a First-Fit strategy to select a candidate host for each VM
                    for cand_server in candidate_servers:
                        if cand_server.has_capacity_to_host(vm):
                            yield SimulationEnvironment.first().env.timeout(vm.migrate(cand_server))
                            break

            if len(server.virtual_machines) == 0:
                servers_being_emptied.append(server)
def describe_simulation_scenario():
    """ Describes the simulation scenario with all created objects and its relationships
    """

    occupation_rate = 0
    for server in Server.all():
        occupation_rate += server.occupation_rate()
    occupation_rate = occupation_rate / Server.count()

    print(f'DATASET NAME: {OUTPUT_FILE_NAME}.json')
    print(f'DATA CENTER OCCUPATION: {round(occupation_rate)}% ({occupation_rate})')
def random_fit():
    """ Migrates VMs to random Server objects with resources to host them.
    """

    for vm in VirtualMachine.all():
        random_server = random.choice(Server.all())

        while not random_server.has_capacity_to_host(vm):
            random_server = random.choice(Server.all())


        # Assigning the random server to host the VM
        random_server.virtual_machines.append(vm)

        # Computing VM demand to its host server
        random_server.cpu_demand += vm.cpu_demand
        random_server.memory_demand += vm.memory_demand
        random_server.disk_demand += vm.disk_demand

        # Assigning the server as the VM host
        vm.server = random_server
Example #6
0
def best_fit(ordering=None):
    """ Best-Fit heuristic (tries to allocate each VM inside the server that has the maximum load,
    but still has resources to host the VM).
    For pragmatism purposes, we can define the way VMs are ordered. By default, we can
    select "Increasing" or "Decreasing". If we don't pass the ordering parameter, the function
    will define a placement for VMs based on the ordering provided by the input file.

    Parameters
    ==========
    ordering : String
        Ordering of VMs to be used by the Best-Fit heuristic
    """

    # Ordering VMs before selecting the VM placement
    vms = vms_ordering(ordering=ordering)

    for vm in vms:
        # As the Best-Fit heuristic consists of choosing the server with maximum
        # load that still has resources to host the VM, we choose for sorting servers
        # according to their demand (descending), and then we pick the first server
        # in that list with resources to host the VM
        servers = sorted(Server.all(),
                         key=lambda sv:
                         (-sv.cpu_demand, -sv.memory_demand, -sv.disk_demand))

        for server in servers:
            if server.has_capacity_to_host(vm):

                server.virtual_machines.append(vm)

                server.cpu_demand += vm.cpu_demand
                server.memory_demand += vm.memory_demand
                server.disk_demand += vm.disk_demand

                vm.server = server

                break
def worst_fit_like():
    """
    Worst-Fit-like maintenance strategy (presented by Severo et al.)
    ====================================================================
    Note: We use the term "empty" to refer to servers that are not hosting VMs.

    The maintenance process is divided in two tasks:
    (i) Patching empty servers (lines 25-35)
    (ii) Migrating VMs to empty more servers (lines 40-72)

    When choosing which servers will host the VMs, this
    strategy uses the Worst-Fit Decreasing heuristic.
    """

    # Patching servers nonupdated servers that are not hosting VMs
    servers_to_patch = Server.ready_to_patch()
    if len(servers_to_patch) > 0:
        servers_patch_duration = []

        for server in servers_to_patch:
            patch_duration = server.update()
            servers_patch_duration.append(patch_duration)

        # As servers are updated simultaneously, we don't need to call the function
        # that quantifies the server maintenance duration for each server being patched
        yield SimulationEnvironment.first().env.timeout(
            max(servers_patch_duration))

    # (ii) Migrating VMs to empty more servers
    else:
        servers_being_emptied = []

        # Getting the list of servers that still need to receive the patch
        servers_to_empty = Server.nonupdated()

        for server in servers_to_empty:
            # We consider as candidate hosts for the VMs all Server
            # objects not being emptied in the current maintenance step
            candidate_servers = [
                cand_server for cand_server in Server.all()
                if cand_server not in servers_being_emptied
                and cand_server != server
            ]

            # Sorting VMs by its demand (decreasing)
            vms = [vm for vm in server.virtual_machines]
            vms = sorted(vms, key=lambda vm: -vm.demand())

            for _ in range(len(server.virtual_machines)):
                vm = vms.pop(0)

                # Sorting servers (bins) to align with Worst-Fit's idea,
                # which is prioritizing servers with less space remaining
                candidate_servers = sorted(
                    candidate_servers, key=lambda cand: cand.occupation_rate())

                # Migrating VMs using the Worst-Fit heuristic
                for cand_server in candidate_servers:
                    if cand_server.has_capacity_to_host(vm):
                        # Migrating the VM and storing the migration duration to allow future analysis
                        yield SimulationEnvironment.first().env.timeout(
                            vm.migrate(cand_server))
                        break

            if len(server.virtual_machines) == 0:
                servers_being_emptied.append(server)
Example #8
0
def vulnerability_surface(env, maintenance_data):
    """
    Maintenance strategy proposed by Severo et al. 2020
    ===================================================
    Note: We use the term "empty" to refer to servers that are not hosting VMs

    When choosing which servers to empty, this heuristic prioritizes servers that
    take less time to be emptied, which can be achieved by having a small number
    of VMs or by hosting small VMs (that will take a negligible time to be migrated).

    The maintenance process is divided in two tasks:
    (i) Patching empty servers (lines 31-38)
    (ii) Migrating VMs to empty more servers (lines 42-90)

    Parameters
    ==========
    env : SimPy.Environment
        Used to quantity the amount of simulation time spent by the migration

    maintenance_data : List
        Object that will be filled during the maintenance, storing metrics on each maintenance step
    """

    while len(Server.nonupdated()) > 0:
        # Patching servers nonupdated servers that are not hosting VMs
        servers_to_patch = Server.ready_to_patch()
        if len(servers_to_patch) > 0:
            for server in servers_to_patch:
                server.updated = True

            # As servers are updated simultaneously, we don't need to call the function
            # that quantifies the server maintenance duration for each server being patched
            yield env.process(server_update(env, constants.PATCHING_TIME))

        # Migrating VMs

        servers_being_emptied = []

        # Sorts the servers to empty based on its update score. This score considers
        # the amount of time needed to migrate all VMs hosted by the server
        servers_to_empty = sorted(
            Server.nonupdated(),
            key=lambda cand_server: cand_server.update_cost())

        migrations_data = [
        ]  # Stores data on the migrations performed to allow future analysis

        for server in servers_to_empty:

            vms_to_migrate = len(server.virtual_machines)
            servers_checked = 0

            # We consider as candidate hosts for the VMs every server
            # not being emptied in the current iteration
            candidate_servers = [
                cand_server for cand_server in Server.all()
                if cand_server not in servers_being_emptied
                and cand_server != server
            ]

            while len(server.virtual_machines
                      ) > 0 and servers_checked < vms_to_migrate * len(
                          candidate_servers):
                # Sorting VMs by its demand (decreasing)
                vms = sorted(
                    server.virtual_machines,
                    key=lambda vm:
                    (-vm.cpu_demand, -vm.memory_demand, -vm.disk_demand))

                vm = server.virtual_machines[0]

                # Sorting servers by update status (updated ones first) and demand (decreasing)
                candidate_servers = sorted(
                    candidate_servers,
                    key=lambda cand_server:
                    (-cand_server.updated, -cand_server.cpu_demand,
                     -cand_server.memory_demand, -cand_server.disk_demand))

                if Server.can_host_vms(candidate_servers, vms):
                    # Using a First-Fit Decreasing strategy to select a candidate host for each VM
                    for cand_server in candidate_servers:
                        servers_checked += 1
                        if cand_server.has_capacity_to_host(vm):

                            # Migrating the VM and storing the migration duration to allow future analysis
                            migration_duration = yield env.process(
                                vm.migrate(env, cand_server))

                            migrations_data.append({
                                'origin':
                                server,
                                'destination':
                                cand_server,
                                'vm':
                                vm,
                                'duration':
                                migration_duration
                            })

                            break
                else:
                    break

            if len(server.virtual_machines) == 0:
                servers_being_emptied.append(server)

        # Collecting metrics gathered in the current maintenance step (i.e., outer while loop iteration)
        maintenance_data.append(
            collect_metrics(env, 'VS Heuristic', servers_to_patch,
                            servers_being_emptied, migrations_data))
Example #9
0
def first_fit(env, maintenance_data):
    """
    First-Fit like maintenance strategy (presented by Severo et al. 2020)
    ====================================================================
    Note: We use the term "empty" to refer to servers that are not hosting VMs.

    The maintenance process is divided in two tasks:
    (i) Patching empty servers (lines 29-36)
    (ii) Migrating VMs to empty more servers (lines 40-73)

    When choosing which servers will host the VMs, this strategy uses the First-Fit heuristic.

    Parameters
    ==========
    env : SimPy.Environment
        Used to quantity the amount of simulation time spent by the migration

    maintenance_data : List
        Object that will be filled during the maintenance, storing metrics on each maintenance step
    """

    while len(Server.nonupdated()) > 0:
        # (i) Patching servers nonupdated servers that are not hosting VMs (lines 30-37)
        servers_to_patch = Server.ready_to_patch()
        if len(servers_to_patch) > 0:
            for server in servers_to_patch:
                server.updated = True

            # As servers are updated simultaneously, we don't need to call the function
            # that quantifies the server maintenance duration for each server being patched
            yield env.process(server_update(env, constants.PATCHING_TIME))

        # (ii) Migrating VMs to empty more servers (lines 41-74)

        servers_being_emptied = []

        # Getting the list of servers that still need to receive the patch
        servers_to_empty = Server.nonupdated()
        migrations_data = [
        ]  # Stores data on the migrations performed to allow future analysis

        for server in servers_to_empty:

            candidate_servers = [
                cand_server for cand_server in Server.all()
                if cand_server != server
                and cand_server not in servers_being_emptied
            ]

            vms_to_migrate = len(server.virtual_machines)
            servers_checked = 0

            while len(server.virtual_machines
                      ) > 0 and servers_checked <= vms_to_migrate * len(
                          candidate_servers):

                vm = server.virtual_machines[0]

                # Migrating VMs using the First-Fit heuristic, which suggests the
                # migration of VMs to the first server that has resources to host it
                for cand_server in candidate_servers:
                    servers_checked += 1
                    if cand_server.has_capacity_to_host(vm):

                        # Migrating the VM and storing the migration duration to allow future analysis
                        migration_duration = yield env.process(
                            vm.migrate(env, cand_server))

                        migrations_data.append({
                            'origin': server,
                            'destination': cand_server,
                            'vm': vm,
                            'duration': migration_duration
                        })

                        break

            if len(server.virtual_machines) == 0:
                servers_being_emptied.append(server)

        # Collecting metrics gathered in the current maintenance step (i.e., outer while loop iteration)
        maintenance_data.append(
            collect_metrics(env, 'First-Fit', servers_to_patch,
                            servers_being_emptied, migrations_data))
Example #10
0
def collect_metrics(env, strategy, servers_patched, servers_being_emptied,
                    migrations_data):
    """ Gather metrics from the current maintenance step.

    Supported metrics:
        - Simulation steps
        - Number of servers being updated
        - Number of servers being emptied
        - Number of updated servers
        - Number of nonupdated servers
        - Vulnerability Surface (Severo et al. 2020)
        - Number of VM migrations
        - Overall migrations duration (amount of time spent with migrations in the current step)
        - Longer migration
        - Shorter migration
        - Average migration duration
        - Servers occupation rate
        - Servers consolidation rate

    Parameters
    ==========
    env : SimPy.Environment
        Used to quantity the amount of simulation time spent by the migration

    strategy : String
        Name of the used maintenance strategy

    servers_patched : List
        List of servers updated in the current maintenance step

    servers_being_emptied : List
        List of servers being emptied in the current maintenance step

    migrations_data : List
        Information on each migration performed in the current maintenance step

    Returns
    =======
    output : Dictionary
        List of metrics collected during the current maintenance step
    """

    output = {}

    # Number of simulation steps
    output['simulation_steps'] = env.now

    # Name of the used maintenance strategy
    output['strategy'] = strategy

    # Other simulation metrics
    output['metrics'] = {}

    # Number of updated and nonupdated servers
    output['metrics']['updated_servers'] = len(Server.updated())
    output['metrics']['nonupdated_servers'] = len(Server.nonupdated())

    # Vulnerability Surface (Severo et al. 2020) = Number of non-updated servers * Elapsed time
    output['metrics']['vulnerability_surface'] = env.now * output['metrics'][
        'nonupdated_servers']

    # Gathering VM migration metrics
    output['metrics']['vm_migrations'] = 0
    output['metrics']['migrations_duration'] = 0
    output['metrics']['longer_migration'] = 0
    output['metrics']['shorter_migration'] = 0
    output['metrics']['avg_migration_duration'] = 0

    if len(migrations_data) > 0:
        # Number of VM migrations performed in this interval
        output['metrics']['vm_migrations'] = len(migrations_data)

        # Time spent performing VM migrations
        migrations_duration = sum(migr['duration'] for migr in migrations_data)
        output['metrics']['migrations_duration'] = migrations_duration

        # Longer migration
        output['metrics']['longer_migration'] = max(
            migr['duration'] for migr in migrations_data)

        # Shorter migration
        output['metrics']['shorter_migration'] = min(
            migr['duration'] for migr in migrations_data)

        # Average migration duration
        output['metrics'][
            'avg_migration_duration'] = migrations_duration / len(
                migrations_data)

    # Gathering server-related metrics
    # Occupation rate
    aggregated_occupation_rate = sum(sv.occupation_rate()
                                     for sv in Server.all())
    output['metrics']['occupation_rate'] = aggregated_occupation_rate / len(
        Server.used_servers())

    # Consolidation rate
    output['metrics']['consolidation_rate'] = Server.consolidation_rate()

    # Servers being updated
    output['metrics']['servers_being_updated'] = len(servers_patched)
    # Servers being emptied
    output['metrics']['servers_being_emptied'] = len(servers_being_emptied)

    return (output)
    TOPOLOGY = fnss.topologies.datacenter.fat_tree_topology(k=8)



######################
## CREATING OBJECTS ##
######################
create_servers()
create_virtual_machines()



###################################################################################
## MAPPING ORIGINAL GRAPH NODES THAT REPRESENT HOSTS TO PYTHON OBJECTS (SERVERS) ##
###################################################################################
servers = [sv for sv in Server.all()]
new_nodes = []
for node in TOPOLOGY.nodes(data=True):
    if node[1]['type'] == 'host':
        new_nodes.append(servers.pop(0))
    else:
        new_nodes.append(node[0])


TOPOLOGY = map_graph_nodes_to_objects(TOPOLOGY, new_nodes)


# Updating created object's topology property
for server in Server.all():
    server.topology = TOPOLOGY
Example #12
0
def salus():
    """ Salus is the Roman goddess of safety. This maintenance
    strategy was proposed by Severo et al. in 2020.
    
    Note: We use the term "empty" to refer to servers that are not hosting VMs

    When choosing which servers to empty, this heuristic prioritizes servers with
    a smaller update cost, which takes into account multiple factors such as server
    capacity and server's patch duration.

    The maintenance process is divided in two tasks:
    (i) Patching empty servers (lines 28-36)
    (ii) Migrating VMs to empty more servers (lines 41-74)
    """

    # Patching servers nonupdated servers that are not hosting VMs
    servers_to_patch = Server.ready_to_patch()
    if len(servers_to_patch) > 0:
        servers_patch_duration = []

        for server in servers_to_patch:
            patch_duration = server.update()
            servers_patch_duration.append(patch_duration)

        # As servers are updated simultaneously, we don't need to call the function
        # that quantifies the server maintenance duration for each server being patched
        yield SimulationEnvironment.first().env.timeout(max(servers_patch_duration))


    # Migrating VMs
    else:
        servers_being_emptied = []

        # Sorts the servers to empty based on its update score. This score considers the amount of time
        # needed to update the server (including VM migrations to draining the server) and its capacity
        servers_to_empty = sorted(Server.nonupdated(), key=lambda sv:
            (sv.maintenance_duration() * (1/(sv.capacity()+1))) ** (1/2))


        for server in servers_to_empty:
            # We consider as candidate hosts for the VMs all Server
            # objects not being emptied in the current maintenance step
            candidate_servers = [cand_server for cand_server in Server.all()
                if cand_server not in servers_being_emptied and cand_server != server]

            # Sorting VMs by its demand (decreasing)
            vms = [vm for vm in server.virtual_machines]
            vms = sorted(vms, key=lambda vm: -vm.demand())

            if Server.can_host_vms(candidate_servers, vms):
                for _ in range(len(server.virtual_machines)):
                    vm = vms.pop(0)

                    # Sorting servers by update status (updated ones first) and demand (decreasing)
                    candidate_servers = sorted(candidate_servers, key=lambda sv:
                        (-sv.updated, -sv.occupation_rate()))

                    # Using a Best-Fit Decreasing strategy to select a candidate host for each VM
                    for cand_server in candidate_servers:
                        if cand_server.has_capacity_to_host(vm):
                            yield SimulationEnvironment.first().env.timeout(vm.migrate(cand_server))
                            break

            if len(server.virtual_machines) == 0:
                servers_being_emptied.append(server)
Example #13
0
    def load_dataset(cls, input_file):
        """ Creates simulation objects according to data from a JSON input file

        Parameters
        ==========
        file : string
            Path location of the JSON input file

        initial_edge_node_connection : boolean, optional
            Informs if the input file provides information on which clients are initially connected to edge nodes

        initial_placement : boolean, optional
            Informs if the input file provides information on the services initial placement
        """

        with open(f'data/{input_file}.json', 'r') as read_file:
            data = json.load(read_file)
            read_file.close()

        # Informing the simulation environment what's the dataset that will be used during the simulation
        Simulator.environment.dataset = input_file

        ##########################
        # SIMULATION COMPONENTS ##
        ##########################
        # Servers
        for server_data in data['servers']:
            # Creating object
            server = Server(id=None,
                            cpu=None,
                            memory=None,
                            disk=None,
                            updated=None)

            # Defining object attributes
            server.id = server_data['id']
            server.cpu_capacity = server_data['cpu_capacity']
            server.memory_capacity = server_data['memory_capacity']
            server.disk_capacity = server_data['disk_capacity']
            server.updated = server_data['updated']
            server.patch_duration = server_data['patch_duration']
            server.sanity_check_duration = server_data['sanity_check_duration']

        # Virtual Machines
        for vm_data in data['virtual_machines']:
            # Creating object
            vm = VirtualMachine(id=None, cpu=None, memory=None, disk=None)

            # Defining object attributes
            vm.id = vm_data['id']
            vm.cpu_demand = vm_data['cpu_demand']
            vm.memory_demand = vm_data['memory_demand']
            vm.disk_demand = vm_data['disk_demand']

            # Initial Placement
            server = Server.find_by_id(vm_data['server'])

            server.cpu_demand += vm.cpu_demand
            server.memory_demand += vm.memory_demand
            server.disk_demand += vm.disk_demand

            vm.server = server
            server.virtual_machines.append(vm)

        ######################
        ## Network Topology ##
        ######################
        topology = FatTree()

        # Creating links and nodes
        for link in data['network_topology']:

            # Creating nodes
            if link['nodes'][0]['type'] == 'Server':
                node_1 = Server.find_by_id(link['nodes'][0]['id'])
            else:
                node_1 = link['nodes'][0]['id']

            # Creating node 1 if it doesn't exists yet
            if node_1 not in topology:
                topology.add_node(node_1)
                for key, value in link['nodes'][0]['data'].items():
                    topology.nodes[node_1][key] = value

            if link['nodes'][1]['type'] == 'Server':
                node_2 = Server.find_by_id(link['nodes'][1]['id'])
            else:
                node_2 = link['nodes'][1]['id']

            # Creating node 2 if it doesn't exists yet
            if node_2 not in topology:
                topology.add_node(node_2)
                for key, value in link['nodes'][1]['data'].items():
                    topology.nodes[node_2][key] = value

            # Creating link if it wasn't created yet
            if not topology.has_edge(node_1, node_2):
                topology.add_edge(node_1, node_2)

                # Adding attributes to the link
                topology[node_1][node_2]['bandwidth'] = link['bandwidth']

        # Assigning 'topology' and 'simulation_environment' attributes to created objects
        objects = Server.all() + VirtualMachine.all()
        for obj in objects:
            obj.topology = topology
            obj.simulation_environment = Simulator.environment
Example #14
0
def show_metrics(dataset, heuristic, output_file=None):
    """ Presents information and metrics of the performed placement
    and optionally stores these results into an output CSV file.

    Currently, this method outputs the following metrics:
        - Servers occupation rate
        - Servers consolidation rate

    Parameters
    ==========
    dataset : String
        Name of the used dataset

    heuristic : STring
        Name of the used placement heuristic

    output_file : String
        Optional parameters regarding the name of the output CSV file
    """

    # Servers occupation rate
    occupation_rate = sum(sv.occupation_rate()
                          for sv in Server.all()) / len(Server.used_servers())

    # Servers consolidation rate
    consolidation_rate = Server.consolidation_rate()

    # Prints out the placement metrics
    print(
        '========================\n== SIMULATION RESULTS ==\n========================'
    )

    print(f'Dataset: "{dataset}"')
    print(f'Placement Strategy: {heuristic}\n')

    print(f'Consolidation Rate: {consolidation_rate}')
    print(f'Occupation Rate: {occupation_rate}\n')

    print('Placement:')
    for server in Server.all():
        vms = [vm.id for vm in server.virtual_machines]
        print(f'SV_{server.id}. VMs: {vms}')

    # If the output_file parameter was provided, stores the placement results into a CSV file
    if output_file:

        with open(output_file, mode='w') as csv_file:
            output_writer = csv.writer(csv_file,
                                       delimiter='\t',
                                       quotechar='"',
                                       quoting=csv.QUOTE_MINIMAL)

            # Creating header
            output_writer.writerow([
                'Dataset', 'Strategy', 'No. of Servers', 'No. of VMs',
                'Occupation Rate', 'Consolidation Rate'
            ])

            # Creating body
            output_writer.writerow([
                dataset, heuristic,
                Server.count(),
                VirtualMachine.count(), occupation_rate, consolidation_rate
            ])