The Scale App is a specialized Phenix application designed for high-volume simulations. Unlike standard apps that map one host in the topology to one configuration, the Scale app uses Plugins to algorithmically generate large-scale topologies (hundreds or thousands of nodes) from concise metadata profiles.
The app is designed to be used in two ways:
- Nested Containers (Primary): Running multiple containers inside VMs that have Minimega installed within them. This allows for high-density simulations where a single VM hosts many lightweight containers.
- VM Scaling: Simply deploying a large number of VMs by themselves without nested containers. This mode is illustrated by the
builtinplugin, which can scale standard VMs based on a count.
The Scale app operates on a Plugin Architecture. The core app handles the generic Phenix lifecycle, directory management, and Minimega interaction. It delegates the specific logic of what to build (VM specs, IP addresses, internal configurations) to registered plugins.
graph TD
subgraph "Configure Stage"
Profile[Metadata Profile] -->|Input| Plugin
Plugin -->|Calculates| NodeCount[Node Count]
Plugin -->|Generates| NodeSpec[VM Specifications]
NodeSpec -->|Adds to| Topology[Phenix Topology]
end
subgraph "Post-Start Stage"
Topology -->|Input| PluginPost["Plugin (Re-init)"]
PluginPost -->|Generates| MMConfig["Minimega Config (.mm)"]
MMConfig -->|Injected into| VM[Virtual Machine]
end
subgraph "Runtime (Inside VM)"
VM -->|Runs| MMCC[Minimega CC]
MMCC -->|Launches| Container1["Container 1 (Optional)"]
MMCC -->|Launches| ContainerN["Container N (Optional)"]
MMCC -->|Configures| Network[Networking]
end
Because Phenix orchestrators run lifecycle stages as separate process executions, the plugin lifecycle is split into two distinct phases.
This stage defines the experiment topology (VMs, networks, injections).
- Validates profiles.
- Calculates node counts.
- Generates VM specifications (CPU, RAM, Image).
- Adds nodes to the Phenix topology.
This stage runs after VMs are active. It generates runtime configurations (Minimega files).
- Generates Minimega (
.mm) configuration files for each node. - Injects runtime configurations into VMs.
- Configures networking (IPs, Routes) inside the VMs via Minimega Command and Control (CC).
The Scale app is typically invoked by the Phenix orchestrator based on the scenario metadata.
The app expects a profiles list in its metadata. Each profile defines a set of resources managed by a specific plugin.
spec:
scenario:
apps:
- name: scale
metadata:
profiles:
- name: my-cluster
plugin: builtin
containers: 50
containers_per_node: 5
node_template:
image: ubuntu.qc2
cpu: 2
memory: 2048
container_template:
cpu: 1
memory: 512
rootfs: otsimfs.tgz
gateway: MGMT
networks:
- name: MGMT
network: 172.16.0.0/24| Field | Description |
|---|---|
name |
Unique identifier for the profile. |
plugin |
Name of the plugin to use (e.g., builtin, wind_turbine). Can be a string or a dict {name: "...", version: "..."}. |
count |
(Optional) Primary scaling factor. Meaning depends on the plugin (e.g., number of VMs or number of assets). |
containers |
(Optional) Total number of containers to deploy. Used by builtin plugin. |
containers_per_node |
(Optional) Density of containers per VM. |
node_template |
Overrides for VM hardware (cpu, memory, image, network). |
container_template |
Configuration for the workload inside the VM (e.g., cpu, memory, rootfs, networks, gateway). |
start_scripts |
List of local file paths to inject and run at startup on every node. |
While the Scale app provides a common schema, plugins are free to interpret these fields according to their domain logic.
Example: The count field
builtinPlugin:countrefers to the number of Virtual Machines to deploy.wind_turbinePlugin:countrefers to the number of Wind Turbines (assets) to simulate. Since one turbine consists of 6 containers, the plugin calculates the actual number of VMs based oncontainers_per_node.
The Scale app supports multiple versions of the same plugin.
- Default: If
versionis omitted, the app loads the latest version (based on semantic versioning, e.g.,2.0.0>1.0.0). - Explicit: You can pin a specific version in the profile.
- Deprecation: If a requested plugin version is marked as deprecated, a warning will be logged at runtime.
plugin:
name: builtin
version: "1.0.0"The default plugin for generic infrastructure scaling.
Modes:
- VM Scaling: Set
count. Createscountnumber of VMs. - Container Scaling: Set
containersandcontainers_per_node. Calculates required VMs to host the containers.
Example:
- name: web-servers
plugin: builtin
containers: 100
containers_per_node: 10
# Result: 10 VMs createdA domain-specific plugin for simulating wind farms.
count: Number of Wind Turbines (not VMs).containers_per_node: Number of containers per VM.- Logic: Each turbine consists of 6 containers.
- Total Containers =
count* 6. - Total VMs =
Total Containers/containers_per_node.
- Total Containers =
Example:
- name: wind-farm
plugin: wind_turbine
count: 30 # 30 Turbines (180 containers)
containers_per_node: 18 # 3 Turbines per VM
# Result: 10 VMs createdPlugins implement the ScalePlugin interface. The Scale app calls these methods at specific points in its lifecycle.
validate_profile(app, profile): Called first to validate the user's profile configuration.pre_configure(app, profile): Called to initialize the plugin with the profile data.get_node_count(): Called to determine how many VMs to create.- Loop over nodes (1 to count):
get_node_spec(index): Returns the VM specification (hardware, network) for the node.get_hostname(index): Returns the hostname for the node.get_additional_startup_commands(index, hostname): Returns bash commands to run on VM startup (before Minimega).on_node_configured(app, index, hostname): Called after the node is added to the topology. Useful for generating side-car configs or injections.
pre_post_start(app, profile): Called to re-initialize the plugin state (since this is a new process execution).get_node_count(): Called again to iterate nodes.- Loop over nodes (1 to count):
get_hostname(index): Resolves hostname.get_container_count(index): Returns the number of containers to launch on this VM.update_template_config(cfg): Allows the plugin to inject custom variables into the Minimega template context.get_template_name(): Returns the name of the Minimega template to use (default:minimega.mako).
| Method | Description | Required |
|---|---|---|
validate_profile |
Validates profile fields. Default checks for name and plugin. |
No |
pre_configure |
Setup plugin state from profile. | Yes |
get_node_count |
Returns total VMs to deploy. | Yes |
get_node_spec |
Returns dictionary defining VM hardware/network. | Yes |
get_hostname |
Returns hostname string for node index. | Yes |
on_node_configured |
Hook for post-topology actions (e.g., injections). | Yes |
get_additional_startup_commands |
Bash commands to run in VM boot script. | Yes |
pre_post_start |
Re-setup plugin state for post-start phase. | Yes |
get_container_count |
Returns container count for a specific VM index. | Yes |
get_template_name |
Returns Mako template filename. Default: minimega.mako. |
No |
update_template_config |
Modifies the dictionary passed to the Mako template. | No |
get_plugin_config |
Returns dict for debug logging. | No |
Plugins can override the default Minimega template (minimega.mako) to customize how VMs and containers are launched.
- Define
templates_dir: In your plugin's__init__, setself.templates_dirto the absolute path of your templates directory. - Override
get_template_name: Return the filename of your custom Mako template. - Override
update_template_config: Inject custom variables into theconfigdictionary passed to the template.
Example:
class MyPlugin(ScalePlugin):
def __init__(self):
# Set templates directory relative to this file
self.templates_dir = os.path.join(os.path.dirname(__file__), "templates")
def get_template_name(self) -> str:
return "my_custom_vm.mako"
def update_template_config(self, cfg: dict[str, Any]) -> None:
# Add custom variables for the template
cfg["MY_CUSTOM_VAR"] = "some_value"Plugins can be Internal (built into phenix-apps) or External (installed via separate Python packages).
- Create a class inheriting from
phenix_apps.apps.scale.interface.ScalePlugin. - Implement the abstract methods.
- Decorate with
@register_plugin("my_plugin_name", "1.0.0", deprecated=False). - Register the plugin in
pyproject.tomlunder[project.entry-points."phenix.scale.plugins"].
External plugins allow you to extend the Scale app without modifying the core codebase.
- Create your plugin package structure.
- Implement the plugin class with the
@register_plugindecorator (same as 1-3 of Internal Plugins). - Register an entry point in your
pyproject.toml(orsetup.py) under the groupphenix.scale.plugins.
Example my_plugin.py:
from phenix_apps.apps.scale.interface import ScalePlugin
from phenix_apps.apps.scale.registry import register_plugin
@register_plugin("my-external-plugin")
class MyExternalPlugin(ScalePlugin):
# ... implement abstract methods ...
passExample pyproject.toml:
[project.entry-points."phenix.scale.plugins"]
my-external-plugin = "my_package.my_plugin:MyExternalPlugin"See phenix_apps/apps/scale/interface.py for the API definition.
The core logic and plugin loading mechanisms are tested in phenix_apps/apps/scale/tests/test_scale.py.
To run the tests:
pytest phenix_apps/apps/scale/tests/test_scale.pyYou can execute a dry-run of the Scale app using the provided sample input phenix_apps/apps/scale/tests/test_scale_input.yaml. This verifies that the app can parse profiles and generate the expected topology without interacting with a live Phenix system.
You can also control the log verbosity by setting the PHENIX_LOG_LEVEL environment variable (e.g., debug, info, warning).
To prevent permission errors from writing to system log directories, file logging should be disabled by setting PHENIX_LOG_FILE to an empty string.
# Configure Stage (with debug logging)
PHENIX_LOG_FILE="" PHENIX_LOG_LEVEL=debug phenix-app-scale configure --dry-run < phenix_apps/apps/scale/tests/test_scale_input.yaml
# Post-Start Stage
PHENIX_LOG_FILE="" phenix-app-scale post-start --dry-run < phenix_apps/apps/scale/tests/test_scale_input.yaml