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
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -584,6 +584,8 @@ For each pool target :
2. Get attached secondary hosts of the pool
* Repeat step `1.` for each secondary

It is possible to mark target as *"nested"* (XCP-ng inside VM). *See inventory example below*

**Inventory file**

`update` command can read an inventory file in [TOML v1.0.0](https://toml.io/en/v1.0.0) format:
Expand All @@ -601,8 +603,10 @@ Take a look at an example inventory file:

```toml
# my_inventory.toml
parent = "ip_or_hostname" # master host

[all]
nested = true
enablerepos = ["xcp-ng-base"]

[servers]
Expand All @@ -618,6 +622,6 @@ enablerepos = ["xcp-ng-updates"]
> Config values under `servers` override values under `all`. For instance, the above inventory would produce
> the following python dict:
>
> `{'ip_or_hostname-1': {'enablerepos': ['xcp-ng-base']}, 'ip_or_hostname-2': {'enablerepos': ['xcp-ng-updates']}}`
> `{'parent': 'ip_or_hostname', 'hosts': {'ip_or_hostname-1': {'enablerepos': ['xcp-ng-base']}, 'ip_or_hostname-2': {'enablerepos': ['xcp-ng-updates']}}}`
>
> Using *enablerepo flag* `-e` with inventory is still possible, it won't be used though.
2 changes: 1 addition & 1 deletion lib/pool.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ def __init__(self, master_hostname_or_ip: HostAddress) -> None:
if not master.is_master():
raise NotAMasterHostError(f"Host {master_hostname_or_ip} is not a master host. Pool not created.")
self.master = master
self.hosts = [master]
self.hosts: list[Host] = [master]

# wait for XAPI startup to be done, or we can get "Connection
# refused (calling connect )" when calling self.hosts_uuids()
Expand Down
11 changes: 10 additions & 1 deletion lib/tools/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def _command_update(args: argparse.Namespace) -> None:
if args.inventory:
inventory = load_inventory(args.inventory)
else:
inventory = into_inventory(args.hosts, args.repos)
inventory = into_inventory(args.hosts, args.repos, args.parent_host, args.nested)

update_pools(inventory)

Expand Down Expand Up @@ -53,6 +53,15 @@ def cli() -> None:
dest="repos",
help="repositories to enable when updating",
)
subparser_cmd_update.add_argument(
"-P",
"--parent-host",
type=HostAddress,
help="Address (hostname|ip) of the parent master host (works with '--nested')",
)
subparser_cmd_update.add_argument(
"--nested", action="store_true", default=False, help="Indicate whether hosts are nested or not"
)
subparser_cmd_update.set_defaults(func=_command_update)

args = parser.parse_args()
Expand Down
57 changes: 42 additions & 15 deletions lib/tools/inventory.py
Original file line number Diff line number Diff line change
@@ -1,42 +1,69 @@
"""Inventory for tools scripts.
"""
"""Inventory for tools scripts."""

from __future__ import annotations

import tomllib
from pathlib import Path

from lib.common import HostAddress

def load_inventory(inventory_path: Path) -> dict[str, dict[str, list[str]]]:
"""Create an inventory object from loaded inventory file."""
inventory: dict[str, dict[str, list[str]]] = {}
from typing import TypeAlias, TypedDict

class Server(TypedDict):
enablerepos: list[str]
nested: bool


Servers: TypeAlias = dict[HostAddress, Server]

class Inventory(TypedDict):
hosts: Servers
parent: HostAddress | None

def load_inventory(inventory_path: Path) -> Inventory:
"""Create an inventory object from loaded inventory file."""
with open(inventory_path, "rb") as f:
data = tomllib.load(f)

all = data.get("all", {})
servers = data.get("servers", [])

inventory_hosts: Servers = {}
for server, config in servers.items():
nested = config.get("nested", None)
# We can't use 'False' as fallback value here, because 'False' is falsy...
if nested is None:
nested = all.get("nested", False)

repos = config.get("enablerepos", [])
host = {
"enablerepos": repos or all.get("enablerepos", [])
host: Server = {
"enablerepos": repos or all.get("enablerepos", []),
"nested": nested,
}
inventory[server] = host
inventory_hosts[server] = host

return inventory
return {
"hosts": inventory_hosts,
"parent": data.get("parent", None),
}

def into_inventory(hosts: list[HostAddress], enablerepos: list[str]) -> dict[HostAddress, dict[str, list[str]]]:

def into_inventory(
hosts: list[HostAddress], enablerepos: list[str], parent: HostAddress, nested: bool
) -> Inventory:
"""Create an inventory object from arguments.

Basically, it is used as compatibility when we don't want inventory from file.
"""
inventory: dict[HostAddress, dict[str, list[str]]] = {}

inventory_hosts: Servers = {}
for h in hosts:
host = {
host: Server = {
"enablerepos": enablerepos or [],
"nested": nested or False,
}
inventory[h] = host
inventory_hosts[h] = host

return inventory
return {
"hosts": inventory_hosts,
"parent": parent,
}
28 changes: 28 additions & 0 deletions lib/tools/tasks/snapshot.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
"""Snapshot tasks.
"""
from concurrent.futures import ThreadPoolExecutor
from datetime import date

from lib.host import Host
from lib.vm import VM

from .. import logger

def create_snapshot(host: Host, vm_uuids: list[str]):
"""Create a snapshot for a list of VMs in host.

:param `lib.Host` host:
The target parent which hosts the VMs.
:param list[str] vm_uuids:
uuids of target VMs.
"""
# init VMs list
vms = [VM(uuid, host) for uuid in vm_uuids]

snapshot_name = f"utd-{date.today().strftime('%Y%m%d')}"

logger.debug(f"[{host}] Create snapshot '{snapshot_name}' for VMs: {vm_uuids}")

with ThreadPoolExecutor() as executor:
for vm in vms:
executor.submit(vm.snapshot, None, snapshot_name)
33 changes: 24 additions & 9 deletions lib/tools/tasks/update.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,11 +6,14 @@

from concurrent.futures import ThreadPoolExecutor

from lib.host import Host
from lib.pool import NotAMasterHostError, Pool
from lib.tools.inventory import Inventory
from lib.tools.tasks.snapshot import create_snapshot

from .. import logger

def update_pools(inventory: dict) -> None:
def update_pools(inventory: Inventory) -> None:
"""Updates hosts in pool(s).

.. note::
Expand All @@ -23,24 +26,36 @@ def update_pools(inventory: dict) -> None:
Each host (key) holds its own config data (values, eg: `enablerepos`).
"""
logger.debug(f"Inventory: {inventory}")
inventory_hosts = inventory["hosts"]
# init related pools
pools = []
for h in inventory:
pools: list[Pool] = []
nested_hosts: list[Host] = []
for host in inventory_hosts:
try:
p = Pool(h)
p = Pool(host)
pools.append(p)
if inventory_hosts[host]["nested"]:
# we assume secondary are nested when master is nested
nested_hosts.extend(p.hosts)
except NotAMasterHostError:
logger.warning(f"[{h}] Skipping: not a master host")
logger.warning(f"[{host}] Skipping: not a master host")

with ThreadPoolExecutor() as executor:
for p in pools:
executor.submit(p.master.update, inventory[p.master.hostname_or_ip]["enablerepos"])
executor.submit(p.master.update, inventory_hosts[p.master.hostname_or_ip]["enablerepos"])

# secondary hosts
with ThreadPoolExecutor() as executor:
for p in pools:
# omit first item because it is a primary (master)
for h in p.hosts[1:]:
for secondary in p.hosts[1:]:
# repos are the same as the primary (master)
repos = inventory[p.master.hostname_or_ip]["enablerepos"]
executor.submit(h.update, repos)
repos = inventory_hosts[p.master.hostname_or_ip]["enablerepos"]
executor.submit(secondary.update, repos)

# Snapshot creation
# get ids of VMs for parent host
vm_uuids = [h.get_system_uuid() for h in nested_hosts]
if inventory["parent"] is not None:
parent_pool = Pool(inventory["parent"]) # mandatory for getting an host instance
create_snapshot(parent_pool.master, vm_uuids)
Loading