Skip to content

Commit

Permalink
feat(CLI): StorageTransferAction support
Browse files Browse the repository at this point in the history
This is based on my suggestion to remove the `action` group in #202,
which I think was ill-defined.

Instead:

* Adds an `--actions` flag to `node show` and `group show` to
  show relevant `StorageTransferAction`s affecting the node/group.
* Adds an `--all` flag to `node show` and `group show` to provide a
  way to list everything without having to know what lists what.
* Adds two new commands to manipulate `StorageTransferAction`s:
  * `group auto-sync GROUP NODE` to set/clear the autosync flag
  * `node auto-clean NODE GROUP` to set/clear the autoclean flag

For a given (NODE, GROUP) pair, these two commands modify the same row
in the `StorageTransferAction` table (the pair has to be unique in the
table).  So, there's ambiguity over which command goes with which "noun".
The way I've done it, the command is associated with the object that the
auto-action modifies (auto-sync adds files to GROUP; auto-clean deletes
files from NODE).  There's probably an argument for doing it the other
way; I'm not sure it matters much, so long as we are consistent about
it.

Also: should the command names have a dash in them?  Or would `group
autosync GROUP NODE` etc. be better?
  • Loading branch information
ketiltrout committed Nov 14, 2024
1 parent 56348b1 commit c22981c
Show file tree
Hide file tree
Showing 10 changed files with 642 additions and 4 deletions.
2 changes: 2 additions & 0 deletions alpenhorn/cli/group/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

from ...db import StorageGroup, StorageNode

from .autosync import autosync
from .create import create
from .list import list_
from .modify import modify
Expand All @@ -17,6 +18,7 @@ def cli():
"""Manage Storage Groups."""


cli.add_command(autosync, "auto-sync")
cli.add_command(create, "create")
cli.add_command(list_, "list")
cli.add_command(modify, "modify")
Expand Down
76 changes: 76 additions & 0 deletions alpenhorn/cli/group/autosync.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""alpenhorn group auto-sync command"""

import click
import peewee as pw

from ...db import StorageGroup, StorageNode, StorageTransferAction, database_proxy
from ..cli import echo


@click.command()
@click.argument("group_name", metavar="GROUP")
@click.argument("node_name", metavar="NODE")
@click.option(
"--remove",
is_flag=True,
help="Remove (instead of add) NODE as an auto-sync source.",
)
@click.pass_context
def autosync(ctx, group_name, node_name, remove):
"""Manage auto-sync sources for this group.
This allows you to add (the default) or remove (using --remove)
the StorageNode named NODE as a an auto-sync souce for the Storage
Group named GROUP.
If NODE is added as an auto-sync source for GROUP, then, whenever
a file is added to NODE, it will be automatically synced into the
Group GROUP, so long as the file isn't already present in the Group.
"""

with database_proxy.atomic():
try:
group = StorageGroup.get(name=group_name)
except pw.DoesNotExist:
raise click.ClickException(f"no such group: {group_name}")

try:
node = StorageNode.get(name=node_name)
except pw.DoesNotExist:
raise click.ClickException(f"no such node: {node_name}")

# Sanity check: can't auto-sync within a group
if group == node.group and not remove:
raise click.ClickException(
"can't enable auto-sync: "
f'Node "{node_name}" is in group "{group_name}"'
)

# What's the current state?
try:
action = StorageTransferAction.get(node_from=node, group_to=group)
if action.autosync is not remove:
echo("No change")
ctx.exit()
except pw.DoesNotExist:
# No need to create a record to set autosync to zero
if remove:
echo("No change")
ctx.exit()
action = None

# Upsert the change
if action:
StorageTransferAction.update(autosync=not remove).where(
StorageTransferAction.id == action.id
).execute()
else:
StorageTransferAction.create(
node_from=node, group_to=group, autosync=not remove
)

echo(
'Auto-sync from "'
+ node.name
+ ('" started' if not remove else '" stopped.')
)
48 changes: 46 additions & 2 deletions alpenhorn/cli/group/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,21 +5,32 @@
import peewee as pw
from tabulate import tabulate

from ...db import StorageGroup, StorageNode
from ...db import StorageGroup, StorageNode, StorageTransferAction
from ..cli import echo
from ..node.stats import get_stats


@click.command()
@click.argument("group_name", metavar="GROUP")
@click.option(
"--actions",
is_flag=True,
help="Show post-transfer auto-actions affecting this group.",
)
@click.option("all_", "--all", "-a", is_flag=True, help="Show all additional data.")
@click.option("--node-details", is_flag=True, help="Show details of listed nodes.")
@click.option("--node-stats", is_flag=True, help="Show usage stats of listed nodes.")
def show(group_name, node_details, node_stats):
def show(group_name, actions, all_, node_details, node_stats):
"""Show details of a storage group.
Shows details of the storage group named GROUP.
"""

if all_:
node_details = True
node_stats = True
actions = True

try:
group = StorageGroup.get(name=group_name)
except pw.DoesNotExist:
Expand Down Expand Up @@ -108,3 +119,36 @@ def show(group_name, node_details, node_stats):
echo(" " + node.name)
else:
echo(" none")

# List transfer actions, if requested
if actions:
echo("\nAuto-actions:\n")

# It's possible for there to be entries in the table with nothing
# activated. So filter those out
query = (
StorageTransferAction.select()
.join(StorageNode)
.where(
StorageTransferAction.group_to == group,
(StorageTransferAction.autosync == 1)
| (StorageTransferAction.autoclean == 1),
)
.order_by(StorageTransferAction.node_from_id)
)

data = []
for action in query.execute():
if action.autoclean:
data.append(
(action.node_from.name, "Auto-clean", "File added to this group")
)
if action.autosync:
data.append(
(action.node_from.name, "Auto-sync", "File added to that node")
)

if data:
echo(tabulate(data, headers=["Node", "Action", "Trigger"]))
else:
echo(" none")
2 changes: 2 additions & 0 deletions alpenhorn/cli/node/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
from ...db import ArchiveAcq, ArchiveFile, ArchiveFileCopy, StorageGroup, StorageNode

from .activate import activate
from .autoclean import autoclean
from .clean import clean
from .create import create
from .deactivate import deactivate
Expand All @@ -31,6 +32,7 @@ def cli():


cli.add_command(activate, "activate")
cli.add_command(autoclean, "auto-clean")
cli.add_command(clean, "clean")
cli.add_command(create, "create")
cli.add_command(deactivate, "deactivate")
Expand Down
76 changes: 76 additions & 0 deletions alpenhorn/cli/node/autoclean.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
"""alpenhorn node auto-clean command"""

import click
import peewee as pw

from ...db import StorageGroup, StorageNode, StorageTransferAction, database_proxy
from ..cli import echo


@click.command()
@click.argument("node_name", metavar="NODE")
@click.argument("group_name", metavar="GROUP")
@click.option(
"--remove",
is_flag=True,
help="Remove (instead of add) GROUP as an auto-clean trigger.",
)
@click.pass_context
def autoclean(ctx, group_name, node_name, remove):
"""Manage auto-clean triggers for this node.
This allows you to add (the default) or remove (using --remove)
the StorageGroup named GROUP as a an auto-clean trigger for the
Storage Node named NODE.
If GROUP is added as an auto-clean trigger for NODE, then, whenever
a file is added to GROUP, it will be automatically released for
deletion on NODE.
"""

with database_proxy.atomic():
try:
node = StorageNode.get(name=node_name)
except pw.DoesNotExist:
raise click.ClickException(f"no such node: {node_name}")

try:
group = StorageGroup.get(name=group_name)
except pw.DoesNotExist:
raise click.ClickException(f"no such group: {group_name}")

# Sanity check: can't auto-clean within a group
if group == node.group and not remove:
raise click.ClickException(
"can't enable auto-clean: "
f'Node "{node_name}" is in group "{group_name}"'
)

# What's the current state?
try:
action = StorageTransferAction.get(node_from=node, group_to=group)
if action.autoclean is not remove:
echo("No change")
ctx.exit()
except pw.DoesNotExist:
# No need to create a record to set autoclean to zero
if remove:
echo("No change")
ctx.exit()
action = None

# Upsert the change
if action:
StorageTransferAction.update(autoclean=not remove).where(
StorageTransferAction.id == action.id
).execute()
else:
StorageTransferAction.create(
node_from=node, group_to=group, autoclean=not remove
)

echo(
'Auto-clean trigger: Group "'
+ group.name
+ ('" added' if not remove else '" removed.')
)
48 changes: 46 additions & 2 deletions alpenhorn/cli/node/show.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,22 +3,33 @@
import json
import click
import peewee as pw
from tabulate import tabulate

from ...common.util import pretty_bytes
from ...db import StorageGroup, StorageNode
from ...db import StorageGroup, StorageNode, StorageTransferAction
from ..cli import echo
from .stats import get_stats


@click.command()
@click.argument("name", metavar="NAME")
@click.option(
"--actions",
is_flag=True,
help="Show post-transfer auto-actions affecting this group.",
)
@click.option("all_", "--all", "-a", is_flag=True, help="Show all additional data.")
@click.option("--stats", is_flag=True, help="Show usage stats of the node.")
def show(name, stats):
def show(name, actions, all_, stats):
"""Show details of a Storage Node.
Shows details of the Storage Node named NODE.
"""

if all_:
actions = True
stats = True

try:
node = StorageNode.get(name=name)
except pw.DoesNotExist:
Expand Down Expand Up @@ -96,3 +107,36 @@ def show(name, stats):
echo(" Total Files: " + str(stats["count"]))
echo(" Total Size: " + stats["size"])
echo(" Usage: " + stats["percent"].lstrip() + "%")

# List transfer actions, if requested
if actions:
echo("\nAuto-actions:\n")

# It's possible for there to be entries in the table with nothing
# activated. So filter those out
query = (
StorageTransferAction.select()
.join(StorageGroup)
.where(
StorageTransferAction.node_from == node,
(StorageTransferAction.autosync == 1)
| (StorageTransferAction.autoclean == 1),
)
.order_by(StorageTransferAction.group_to_id)
)

data = []
for action in query.execute():
if action.autoclean:
data.append(
(action.group_to.name, "Auto-clean", "File added to that group")
)
if action.autosync:
data.append(
(action.group_to.name, "Auto-sync", "File added to this node")
)

if data:
echo(tabulate(data, headers=["Group", "Action", "Trigger"]))
else:
echo(" none")
Loading

0 comments on commit c22981c

Please sign in to comment.