Skip to content
Snippets Groups Projects
Verified Commit 378a6466 authored by Karel Koci's avatar Karel Koci :metal:
Browse files

nsfarm/lxd: use connection instance instead of global

It was pretty nasty how original LXD connection was designed. It is ok
to just wrap that to class and pass it around instead of calling
connection from various locations just to be sure that all is
initialized before we use it.
parent ca36bcac
Branches
1 merge request!2Initial development
......@@ -8,7 +8,7 @@ import serial
import serial.tools.miniterm
from pexpect import fdpexpect
from .. import cli
from ..lxd import Container
from ..lxd import LXDConnection, Container, NetInterface
MINITERM_DEFAULT_EXIT = '\x1d' # Ctrl+]
MINITERM_DEFAULT_MENU = '\x14' # Ctrl+T
......@@ -63,9 +63,10 @@ class Board(abc.ABC):
self._pexpect.sendline("")
return cli.Uboot(self._pexpect)
def bootup(self, device_wan, os_branch):
def bootup(self, lxd_connection: LXDConnection, device_wan: NetInterface, os_branch: str) -> cli.Shell:
"""Boot board using TFTP boot. This ensures that board is booted up and ready to accept commands.
lxd_connection: instance of nsfarm.lxd.LXDConnection
device_wan: Wan device to board. This is instance of nsfarm.lxd.NetInterface.
os_branch: Turris OS branch to download medkit from.
......@@ -74,7 +75,7 @@ class Board(abc.ABC):
# First get U-Boot prompt
uboot = self.uboot()
# Now load image from TFTP
with Container("boot", devices=[device_wan, ]) as cont:
with Container(lxd_connection, "boot", devices=[device_wan, ]) as cont:
ccli = cli.Shell(cont.pexpect())
ccli.run(f"prepare_turris_image {os_branch}")
uboot.run('setenv ipaddr 192.168.1.142')
......
from .connection import LXDConnection
from .container import Container
from .device import NetInterface
"""Internal global LXD handle.
"""
import logging
import pylxd
IMAGES_SOURCE = "https://images.linuxcontainers.org"
ROOT_PROFILE = "nsfarm-root"
INTERNET_PROFILE = "nsfarm-internet"
REQUIRED_PROFILES = (ROOT_PROFILE, INTERNET_PROFILE)
# Global LXD handles
images = None
local = None
def _profile_device(profile, check_func):
return any(check_func(dev) for dev in profile.devices.values())
def connect():
"""Make sure that we are connected to LXD.
"""
# Suppress logging of pylxd components
logging.getLogger('ws4py').setLevel(logging.ERROR)
logging.getLogger('urllib3').setLevel(logging.ERROR)
# Initialize LXD connection to linuximages.org
global images
if images is None:
images = pylxd.Client(IMAGES_SOURCE)
# Initialize LXD connection to local server
global local
if local is None:
local = pylxd.Client()
# Verify profiles
for name in REQUIRED_PROFILES:
if not local.profiles.exists(name):
# TODO better exception
raise Exception(f"Missing required LXD profile: {name}")
root = local.profiles.get(ROOT_PROFILE)
internet = local.profiles.get(INTERNET_PROFILE)
if not _profile_device(root, lambda dev: dev["type"] == "disk"):
# TODO better exception
raise Exception("nsfarm-root does not provide disk device")
if not _profile_device(internet, lambda dev: dev["type"] == "nic" and dev["name"] == "internet"):
# TODO better exception
raise Exception("nsfarm-internet does not provide appropriate nic")
"""LXD connection for NSFarm.
"""
import logging
import pylxd
class LXDConnection:
"""This is generic connection handler for LXD handling both connections to local and images server.
"""
IMAGES_SOURCE = "https://images.linuxcontainers.org"
ROOT_PROFILE = "nsfarm-root"
INTERNET_PROFILE = "nsfarm-internet"
def __init__(self):
# Suppress logging of pylxd components
logging.getLogger('ws4py').setLevel(logging.ERROR)
logging.getLogger('urllib3').setLevel(logging.ERROR)
self.local = pylxd.Client()
self.images = pylxd.Client(self.IMAGES_SOURCE)
......@@ -2,6 +2,7 @@
"""
import os
import time
import typing
import itertools
import pathlib
import hashlib
......@@ -9,7 +10,8 @@ import logging
import pexpect
import pylxd
from .. import cli
from . import _lxd
from .connection import LXDConnection
from .device import NetInterface
IMAGE_INIT_PATH = "/nsfarm-init.sh" # Where we deploy initialization script for image
......@@ -23,7 +25,9 @@ class Container:
"""
# TODO log syslog somehow
def __init__(self, img_name, devices=(), internet=True):
def __init__(self, lxd_connection: LXDConnection, img_name: str, devices: typing.List[NetInterface] = (),
internet: bool = True):
self._lxd = lxd_connection
self._name = img_name
self._internet = internet
self._devices = tuple(devices)
......@@ -35,17 +39,15 @@ class Container:
raise Exception(f"There seems to be no file describing image: {self._file_path}")
if not self._dir_path.is_dir():
self._dir_path = None
# Make sure that we are connected to LXD
_lxd.connect()
# Get parent
with open(self._file_path) as file:
# This reads second line of file while initial hash removed
parent = next(itertools.islice(file, 1, 2))[1:].strip()
self._parent = None
if parent.startswith("nsfarm:"):
self._parent = Container(parent[7:])
self._parent = Container(lxd_connection, parent[7:])
elif parent.startswith("images:"):
self._parent = _lxd.images.images.get_by_alias(parent[7:])
self._parent = self._lxd.images.images.get_by_alias(parent[7:])
else:
raise Exception(f"The file has parent from unknown source: {parent}: {self.fpath}")
# Calculate identity hash and generate image name
......@@ -91,8 +93,8 @@ class Container:
"""
if self._lxd_image:
return
if _lxd.local.images.exists(self._image_alias, alias=True):
self._lxd_image = _lxd.local.images.get_by_alias(self._image_alias)
if self._lxd.local.images.exists(self._image_alias, alias=True):
self._lxd_image = self._lxd.local.images.get_by_alias(self._image_alias)
return
# We do not have appropriate image so prepare it
logger.warning("Bootstrapping image: %s", self._image_alias)
......@@ -106,11 +108,11 @@ class Container:
else:
# We have to pull it from images
image_source["mode"] = "pull"
image_source["server"] = _lxd.IMAGES_SOURCE
image_source["server"] = self._lxd.IMAGES_SOURCE
image_source["alias"] = self._parent.fingerprint
container_name = f"nsfarm-bootstrap-{self._name}-{self._hash}"
try:
container = _lxd.local.containers.create({
container = self._lxd.local.containers.create({
'name': container_name,
'profiles': ['nsfarm-root', 'nsfarm-internet'],
'source': image_source
......@@ -121,7 +123,7 @@ class Container:
raise
logger.warning("Other instance is already bootsrapping image probably. "
"Waiting for following container to go away: %s", container_name)
while _lxd.local.containers.exists(container_name):
while self._lxd.local.containers.exists(container_name):
time.sleep(1)
self.prepare_image() # possibly get created image or try again
return
......@@ -163,7 +165,7 @@ class Container:
for device in self._devices:
devices.update(device.acquire(self))
# Create and start container
self._lxd_container = _lxd.local.containers.create({
self._lxd_container = self._lxd.local.containers.create({
'name': self._container_name(),
'ephemeral': True,
'profiles': profiles,
......@@ -180,9 +182,9 @@ class Container:
def _container_name(self, prefix="nsfarm"):
name = f"{prefix}-{self._name}-{os.getpid()}"
if _lxd.local.containers.exists(name):
if self._lxd.local.containers.exists(name):
i = 1
while _lxd.local.containers.exists(f"{name}-{i}"):
while self._lxd.local.containers.exists(f"{name}-{i}"):
i += 1
name = f"{name}-{i}"
return name
......
......@@ -4,8 +4,8 @@ import os
import logging
from datetime import datetime
import dateutil.parser
from . import container
from . import _lxd
from .connection import LXDConnection
from .container import Container, IMGS_DIR
logger = logging.getLogger(__package__)
......@@ -18,11 +18,11 @@ def clean(delta, dry_run=False):
Returns list of (to be) removed images.
"""
_lxd.connect()
connection = LXDConnection()
since = datetime.today() - delta
removed = list()
for img in _lxd.local.images.all():
for img in connection.local.images.all():
if not any(alias.startswith("nsfarm/") for alias in img.aliases):
continue
last_used = dateutil.parser.parse(
......@@ -42,7 +42,7 @@ def all_images():
This collects all *.sh files in imgs directory in root of nsfarm project.
"""
return (imgf[:-3] for imgf in os.listdir(container.IMGS_DIR) if imgf.endswith(".sh"))
return (imgf[:-3] for imgf in os.listdir(IMGS_DIR) if imgf.endswith(".sh"))
def bootstrap(imgs=None):
......@@ -53,11 +53,8 @@ def bootstrap(imgs=None):
Returns True if all images were bootstrapped correctly.
"""
success = True
connection = LXDConnection()
for img in all_images() if imgs is None else imgs:
logger.info("Trying to bootstrap: %s", img)
try:
container.Container(img).prepare_image()
except Exception:
success = False
logger.exception("Bootstrap failed for: %s", img)
Container(connection, img).prepare_image()
return success
import pytest
from nsfarm import lxd
@pytest.fixture(name="connection", scope="module")
def fixture_connection():
return lxd.LXDConnection()
@pytest.mark.parametrize("profile", [
lxd.LXDConnection.ROOT_PROFILE,
lxd.LXDConnection.INTERNET_PROFILE,
])
def test_profiles_exists(connection, profile):
"""Check that all profiles we need are configured in LXD.
"""
assert connection.local.profiles.exists(profile)
def test_profile_root(connection):
"""Minimal sanity check of root profile.
"""
profile = connection.local.profiles.get(lxd.LXDConnection.ROOT_PROFILE)
assert any(dev for dev in profile.devices.values() if dev["type"] == "disk")
def test_profile_internet(connection):
"""Minimal sanity check of internet profile.
"""
profile = connection.local.profiles.get(lxd.LXDConnection.INTERNET_PROFILE)
assert any(dev for dev in profile.devices.values() if dev["type"] == "nic" and dev["name"] == "internet")
......@@ -70,6 +70,14 @@ def fixture_board(request):
return brd
# TODO probably mark this with some LXD available/configured mark
@pytest.fixture(scope="session", name="lxd")
def fixture_lxd(request):
"""Provides access to nsfarm.lxd.LXDConnection instance.
"""
return nsfarm.lxd.LXDConnection()
@pytest.fixture(scope="session", name="wan", params=[pytest.param(None, marks=pytest.mark.wan)])
def fixture_wan(request):
"""Top level fixture used to share WAN interface handler.
......@@ -88,12 +96,12 @@ def fixture_lan1(request):
# Boot and setup fixtures ##############################################################################################
@pytest.fixture(name="board_serial", scope="session")
def fixture_board_serial(request, board, wan):
def fixture_board_serial(request, lxd, board, wan):
"""Boot board to Shell.
Provides instance of nsfarm.cli.Shell()
"""
request.addfinalizer(lambda: board.reset(True))
return board.bootup(wan, request.config.target_branch)
return board.bootup(lxd, wan, request.config.target_branch)
@pytest.fixture(name="board_root_password", scope="session")
......@@ -133,10 +141,10 @@ def fixture_client_board(board_serial, board_root_password, lan1_client):
# Common containers ####################################################################################################
@pytest.fixture(name="lan1_client", scope="module")
def fixture_lan1_client(lan1):
def fixture_lan1_client(lxd, lan1):
"""Starts client container on LAN1 and provides it.
"""
with nsfarm.lxd.Container('client', devices=[lan1, ], internet=False) as container:
with nsfarm.lxd.Container(lxd, 'client', devices=[lan1, ], internet=False) as container:
yield container
......@@ -144,13 +152,13 @@ def fixture_lan1_client(lan1):
# Standard configuration ###############################################################################################
@pytest.fixture(name="basic_isp", scope="module")
def fixture_basic_isp(board, client_board, wan):
def fixture_basic_isp(lxd, board, client_board, wan):
"""Basic config we consider general. It provides you with configured WAN.
Returns handle for ISP container on WAN interface.
"""
# TODO what about other settings that are part of guide
with nsfarm.lxd.Container('isp-common', devices=[wan, ]) as container:
with nsfarm.lxd.Container(lxd, 'isp-common', devices=[wan, ]) as container:
client_board.run("uci set network.wan.proto='static'")
client_board.run("uci set network.wan.ipaddr='172.16.1.42'")
client_board.run("uci set network.wan.netmask='255.240.0.0'")
......
......@@ -16,11 +16,11 @@ class TestStatic(common.InternetTests):
"""
@pytest.fixture(scope="class", autouse=True)
def client(self, board, client_board, wan):
def client(self, lxd, board, client_board, wan):
"""Configure WAN to use static IP
"""
print("We are in client fixture once")
with nsfarm.lxd.Container('isp-common', devices=[wan, ]):
with nsfarm.lxd.Container(lxd, 'isp-common', devices=[wan, ]):
# TODO implement some utility class to set and revert uci configs on router
client_board.run("uci set network.wan.proto='static'")
client_board.run("uci set network.wan.ipaddr='172.16.1.42'")
......@@ -45,10 +45,10 @@ class TestDHCP(common.InternetTests):
"""
@pytest.fixture(scope="class", autouse=True)
def client(self, board, client_board, wan):
def client(self, lxd, board, client_board, wan):
"""Configure WAN to use DHCP
"""
with nsfarm.lxd.Container('isp-dhcp', devices=[wan, ]):
with nsfarm.lxd.Container(lxd, 'isp-dhcp', devices=[wan, ]):
client_board.run("uci set network.wan.proto='dhcp'")
client_board.run("uci commit network")
client_board.run("/etc/init.d/network restart")
......
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment