Skip to content
Draft
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
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ jobs:
- name: Create tarball of log archive
if: success() || failure()
run: |
tar -czf /tmp/artifacts/archive.tar ~/.local/share/ceph-devstack/archive/
tar -czf /tmp/artifacts/archive.tar ~/.local/share/ceph-devstack/teuthology/archive/
- name: Upload log archive
if: success() || failure()
uses: actions/upload-artifact@v7.0.1
Expand Down
5 changes: 3 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,8 @@ pool state file and its lock always stay on the local machine.
Each container picks host loop devices at create time: it needs
`loop_device_count` devices of `loop_device_size`, reuses any existing backing
files for that container name, then takes the lowest free loop numbers after
scanning what is already attached or claimed in the `disk_images` directory.
scanning what is already attached or claimed in any stack's `disk_images`
directory.

If `block_pool.json` is lost but the on-disk marker remains, start again to
reclaim the marker; the device is not reformatted. The old allocations are
Expand Down Expand Up @@ -146,7 +147,7 @@ Once everything is started, a message similar to this will be logged:

`View test results at http://smithi065.front.sepia.ceph.com:8081/`

This link points to the running Pulpito instance. Test archives are also stored in the `--data-dir` (default: `~/.local/share/ceph-devstack`).
This link points to the running Pulpito instance. Test archives are also stored in `<data_dir>/teuthology/archive` (`data_dir` defaults to `~/.local/share/ceph-devstack`).

To watch teuthology's output, you can:

Expand Down
10 changes: 10 additions & 0 deletions ceph_devstack/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,11 @@ def parse_args(args: List[str]) -> argparse.Namespace:
default=DEFAULT_CONFIG_PATH,
help="Path to the ceph-devstack config file",
)
parser.add_argument(
"--stack",
default=None,
help="Stack to deploy (default: value of 'stack' in config)",
)
subparsers = parser.add_subparsers(dest="command")
parser_config = subparsers.add_parser("config", help="Get or set config items")
subparsers_config = parser_config.add_subparsers(dest="config_op")
Expand Down Expand Up @@ -216,3 +221,8 @@ def unset_value(self, name: str) -> None:

config = Config()
config.load()


def base_data_dir() -> Path:
"""The configured data_dir; each stack keeps its state in a subdirectory."""
return Path(config.get("data_dir", DEFAULT_DATA_DIR)).expanduser().absolute()
77 changes: 42 additions & 35 deletions ceph_devstack/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import logging
import sys

from pathlib import Path
from argparse import Namespace
from subprocess import CalledProcessError

from ceph_devstack import config, logger, parse_args, VERBOSE
from ceph_devstack.block_pool import BlockPoolError, format_size, shared_pool
Expand All @@ -16,22 +17,6 @@
"unset": lambda config, args: config.unset_value(args.name),
}

COMMAND_HANDLERS = {
"doctor": None,
"apply": lambda args, obj: obj.apply(args.command),
"pull": lambda _, obj: obj.pull(),
"build": lambda _, obj: obj.build(),
"create": lambda _, obj: obj.create(),
"remove": lambda _, obj: obj.remove(),
"start": lambda _, obj: obj.start(),
"stop": lambda _, obj: obj.stop(),
"watch": lambda _, obj: obj.watch(),
"wait": lambda args, obj: obj.wait(container_name=args.container),
"logs": lambda args, obj: obj.logs(
run_name=args.run_name, job_id=args.job_id, locate=args.locate
),
}


def _configure_console_logging(verbose: bool) -> None:
if not verbose:
Expand Down Expand Up @@ -67,6 +52,36 @@ def block_pool_status() -> int:
return 0


def _action_kwargs(args: Namespace) -> dict:
match args.command:
case "wait":
return {"container_name": args.container}
case "logs":
return {
"run_name": args.run_name,
"job_id": args.job_id,
"locate": args.locate,
}
case _:
return {}


async def _apply(stack: CephDevStack, action: str, **kwargs) -> int:
"""Check requirements, then run the action (if any) on the stack."""
if not all(
await asyncio.gather(
check_requirements(),
stack.check_requirements(),
)
):
logger.error("Requirements not met!")
return 1
if action == "doctor":
return 0
result = await stack.apply(action, **kwargs)
return result if result is not None else 0


def main() -> int:
args = parse_args(sys.argv[1:])
config.load(args.config_file)
Expand All @@ -80,25 +95,17 @@ def main() -> int:
return 2
return block_pool_status()
config["args"] = vars(args)
data_path = Path(config["data_dir"]).expanduser()
data_path.mkdir(parents=True, exist_ok=True)
obj = CephDevStack()

async def run():
if not all(
await asyncio.gather(
check_requirements(),
obj.check_requirements(),
)
):
logger.error("Requirements not met!")
return 1
handler = COMMAND_HANDLERS.get(args.command)
if handler:
return await handler(args, obj)

try:
sys.exit(asyncio.run(run()))
stack = CephDevStack(stack_name=args.stack)
except ValueError as exc:
logger.error(str(exc))
return 1
stack.data_dir.mkdir(parents=True, exist_ok=True)
action = args.command
try:
return asyncio.run(_apply(stack, action, **_action_kwargs(args)))
except CalledProcessError as exc:
return exc.returncode if exc.returncode is not None else 1
except KeyboardInterrupt:
logger.debug("Exiting!")
return 130 # 128 + SIGINT
13 changes: 13 additions & 0 deletions ceph_devstack/config.toml
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
stack = "teuthology"
data_dir = "~/.local/share/ceph-devstack"

[block_pool]
Expand All @@ -14,6 +15,18 @@ data_dir = "~/.local/share/ceph-devstack"
# allow_enroll = true
# state_dir = "/path" # where block_pool.json lives; defaults to data_dir

[stacks.teuthology]
services = [
"postgres",
"paddles",
"beanstalk",
"pulpito",
"teuthology",
"testnode",
"archive",
]
secrets = ["ssh_keypair"]

[containers.archive]
image = "python:alpine"

Expand Down
56 changes: 54 additions & 2 deletions ceph_devstack/resources/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,12 +6,42 @@

from pathlib import Path
from subprocess import CalledProcessError
from typing import List, Dict, Set
from typing import List, Dict, Optional, Protocol, Set

from ceph_devstack import config
from ceph_devstack.exec import Subprocess
from ceph_devstack.host import host, local_host


class StackResource(Protocol):
"""Interface that CephDevStack expects from every service it manages."""

def __init__(
self, name: str = "", *, data_dir: Path, containers_config: dict
) -> None: ...

@property
def name(self) -> str: ...

async def pull(self) -> None: ...

async def build(self) -> None: ...

async def create(self) -> None: ...

async def start(self) -> None: ...

async def stop(self) -> None: ...

async def remove(self) -> None: ...

async def exists(self) -> bool: ...

async def is_running(self) -> bool: ...

async def wait(self) -> int | None: ...


class DevStack:
def __init__(self, args: argparse.Namespace):
self.args = args
Expand Down Expand Up @@ -40,16 +70,38 @@ class PodmanResource:
log: Dict[str, Set[str]] = {}
_name: str | None = None

def __init__(self, name: str = ""):
def __init__(
self,
name: str = "",
*,
data_dir: Optional[Path] = None,
containers_config: Optional[dict] = None,
):
if name:
self._name = name
self._data_dir = data_dir
self._containers_config = containers_config

@property
def name(self) -> str:
if self._name is not None:
return self._name
return self.__class__.__name__.lower()

@property
def data_dir(self) -> Path:
"""Where this resource keeps its on-disk state; set by its stack."""
if self._data_dir is None:
raise ValueError(f"{self.name}: no data_dir was given")
return self._data_dir

@property
def containers_config(self) -> dict:
"""The stack's merged [containers] config, else the global one."""
if self._containers_config is None:
return config["containers"]
return self._containers_config

async def cmd(
self,
args: List[str],
Expand Down
Loading