Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions core/frontend/src/components/ethernet/EthernetManager.vue
Original file line number Diff line number Diff line change
Expand Up @@ -3,20 +3,20 @@
elevation="1"
width="400"
>
<v-expansion-panels v-if="are_interfaces_available && !updating_interfaces">
<v-expansion-panels v-show="are_interfaces_available && !updating_interfaces">
<interface-card
v-for="(ethernet_interface, key) in available_interfaces"
:key="key"
:adapter="ethernet_interface"
/>
</v-expansion-panels>
<v-container v-else-if="updating_interfaces">
<v-container v-if="updating_interfaces">
<spinning-logo
size="30%"
subtitle="Fetching available ethernet interfaces..."
/>
</v-container>
<v-container v-else>
<v-container v-else-if="!are_interfaces_available">
<div>
No ethernet interfaces available
</div>
Expand Down
12 changes: 9 additions & 3 deletions core/frontend/src/components/ethernet/EthernetUpdater.vue
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,17 @@ export default Vue.extend({
name: 'EthernetUpdater',
data() {
return {
fetch_available_interfaces_task: new OneMoreTime({ delay: 5000, disposeWith: this }),
fetch_available_interfaces_task: new OneMoreTime({ delay: 5000, disposeWith: this, autostart: false }),
}
},
mounted() {
this.fetch_available_interfaces_task.setAction(this.fetchAvailableEthernetInterfaces)
async mounted() {
try {
await this.fetchAvailableEthernetInterfaces()
} finally {
ethernet.setUpdatingInterfaces(false)
this.fetch_available_interfaces_task.setAction(this.fetchAvailableEthernetInterfaces)
this.fetch_available_interfaces_task.start()
}
},
methods: {
async fetchAvailableEthernetInterfaces(): Promise<void> {
Expand Down
4 changes: 0 additions & 4 deletions core/frontend/src/components/ethernet/InterfaceCard.vue
Original file line number Diff line number Diff line change
Expand Up @@ -319,16 +319,12 @@ export default Vue.extend({
await ethernet.deleteAddress({ interface_name: this.adapter.name, ip_address: ip })
},
async triggerForDynamicIP(): Promise<void> {
ethernet.setUpdatingInterfaces(true)

await ethernet.triggerDynamicIP(this.adapter.name)
},
openDHCPServerDialog(): void {
this.show_dhcp_server_dialog = true
},
async removeDHCPServer(): Promise<void> {
ethernet.setUpdatingInterfaces(true)

await ethernet.RemoveDHCPServer(this.adapter.name)
},
async fetchLeases(): Promise<void> {
Expand Down
34 changes: 32 additions & 2 deletions core/frontend/src/store/ethernet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ class EthernetStore extends VuexModule {
@Mutation
setInterfaces(ethernet_interfaces: EthernetInterface[]): void {
this.available_interfaces = ethernet_interfaces
this.updating_interfaces = false
}

@Action
Expand All @@ -52,6 +51,10 @@ class EthernetStore extends VuexModule {
notifier.pushBackError('ETHERNET_ADDRESS_CREATION_FAIL', error)
throw error
})
.finally(async () => {
await this.context.dispatch('refreshInterfaces')
this.context.commit('setUpdatingInterfaces', false)
})
}

@Action
Expand All @@ -71,6 +74,10 @@ class EthernetStore extends VuexModule {
notifier.pushError('ETHERNET_ADDRESS_DELETE_FAIL', error)
throw error
})
.finally(async () => {
await this.context.dispatch('refreshInterfaces')
this.context.commit('setUpdatingInterfaces', false)
})
}

@Action
Expand All @@ -91,13 +98,16 @@ class EthernetStore extends VuexModule {
notifier.pushBackError('DHCP_SERVER_ADD_FAIL', error)
throw error
})
.finally(() => {
.finally(async () => {
await this.context.dispatch('refreshInterfaces')
this.context.commit('setUpdatingInterfaces', false)
})
}

@Action
async RemoveDHCPServer(interface_name: string): Promise<void> {
this.context.commit('setUpdatingInterfaces', true)

await back_axios({
method: 'delete',
url: `${this.API_URL}/dhcp`,
Expand All @@ -110,6 +120,10 @@ class EthernetStore extends VuexModule {
const message = `Could not remove DHCP server from interface '${interface_name}': ${error.message}.`
notifier.pushError('DHCP_SERVER_REMOVE_FAIL', message)
})
.finally(async () => {
await this.context.dispatch('refreshInterfaces')
this.context.commit('setUpdatingInterfaces', false)
})
}

@Action
Expand Down Expand Up @@ -189,6 +203,16 @@ class EthernetStore extends VuexModule {
})
}

@Action
async refreshInterfaces(): Promise<void> {
try {
const response = await this.context.dispatch('getAvailableEthernetInterfaces')
this.context.commit('setInterfaces', response.data)
} catch {
// Errors are already pushed to the notifier by getAvailableEthernetInterfaces.
}
}

@Action
async setInterfacesPriority(interfaces: { name: string, priority: number }[]): Promise<void> {
await back_axios({
Expand All @@ -213,6 +237,8 @@ class EthernetStore extends VuexModule {

@Action
async triggerDynamicIP(interface_name: string): Promise<void> {
this.context.commit('setUpdatingInterfaces', true)

await back_axios({
method: 'post',
url: `${this.API_URL}/dynamic_ip`,
Expand All @@ -225,6 +251,10 @@ class EthernetStore extends VuexModule {
const message = `Could not trigger for dynamic IP address on '${interface_name}': ${error.message}.`
notifier.pushError('DYNAMIC_IP_TRIGGER_FAIL', message)
})
.finally(async () => {
await this.context.dispatch('refreshInterfaces')
this.context.commit('setUpdatingInterfaces', false)
})
}
}

Expand Down
8 changes: 8 additions & 0 deletions core/libs/commonwealth/src/commonwealth/utils/decorators.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
def temporary_cache(timeout_seconds: float = 10) -> Callable[[F], F]:
"""Decorator that creates a cache for specific inputs with a configured timeout in seconds.

The wrapped function exposes an `invalidate()` attribute that drops every cached entry,
forcing the next call to re-execute the function.

Args:
timeout_seconds (float, optional): Timeout to be used for cache invalidation. Defaults to 10.

Expand All @@ -35,6 +38,11 @@ def wrapper(*args: Any) -> Any:
cache[args] = function_return
return function_return

def invalidate() -> None:
cache.clear()
last_sample_time.clear()

wrapper.invalidate = invalidate # type: ignore[attr-defined]
return wrapper # type: ignore

return inner_function
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,16 @@ def test_nested_settings_save_load() -> None:

# Check if all cache values are invalid after waiting for a long time
assert all(original_output[key] != cached_function(key) for key in inputs)


def test_temporary_cache_invalidate() -> None:
inputs = ["first", "second", "third"]
original_output = {key: cached_function(key) for key in inputs}

# Cache is still warm: same call returns the same value
assert all(original_output[key] == cached_function(key) for key in inputs)

# Force invalidation; subsequent calls must re-execute the function
cached_function.invalidate() # type: ignore[attr-defined]

assert all(original_output[key] != cached_function(key) for key in inputs)
3 changes: 3 additions & 0 deletions core/services/cable_guy/api/manager.py
Original file line number Diff line number Diff line change
Expand Up @@ -312,6 +312,8 @@ def _update_interface_settings(self, interface_name: str, updated_interface: Net
self._settings.content = [interface for interface in self._settings.content if interface.name != interface_name]
self._settings.content.append(updated_interface)
self._manager.save()
# OS state has just been mutated; drop the cached view so the next read returns fresh data.
self.get_ethernet_interfaces.invalidate() # type: ignore[attr-defined]

def add_static_ip(self, interface_name: str, ip: str, mode: AddressMode = AddressMode.Unmanaged) -> None:
"""Set ip address for a specific interface and saves it to the settings file
Expand Down Expand Up @@ -477,6 +479,7 @@ def get_interfaces(self, filter_wifi: bool = False, include_dhcp_markers: bool =

return result

@temporary_cache(timeout_seconds=10)
def get_ethernet_interfaces(self, include_dhcp_markers: bool = False) -> List[NetworkInterface]:
"""Get ethernet interfaces information

Expand Down
1 change: 0 additions & 1 deletion core/services/cable_guy/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,6 @@

@app.get("/ethernet", response_model=List[NetworkInterface], summary="Retrieve ethernet interfaces.")
@version(1, 0)
@temporary_cache(timeout_seconds=10)
def retrieve_ethernet_interfaces() -> Any:
"""REST API endpoint to retrieve the configured ethernet interfaces."""
return manager.get_ethernet_interfaces()
Expand Down