Compare commits
No commits in common. "master" and "v0.1.0-dev3" have entirely different histories.
master
...
v0.1.0-dev
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,6 +1,6 @@
|
||||
dist/
|
||||
docs/build/
|
||||
packaging/*/build/
|
||||
packaging/build/
|
||||
instance.yaml
|
||||
.ruff_cache/
|
||||
__pycache__/
|
||||
|
22
Makefile
22
Makefile
@ -5,7 +5,7 @@ DOCS_BUILDDIR = docs/build
|
||||
|
||||
.PHONY: docs
|
||||
|
||||
all: build debian archlinux
|
||||
all: docs build-deb
|
||||
|
||||
requirements.txt:
|
||||
poetry export -f requirements.txt -o requirements.txt
|
||||
@ -13,11 +13,8 @@ requirements.txt:
|
||||
build: version format lint
|
||||
poetry build
|
||||
|
||||
debian:
|
||||
cd packaging/debian && $(MAKE)
|
||||
|
||||
archlinux:
|
||||
cd packaging/archlinux && $(MAKE)
|
||||
build-deb: build
|
||||
cd packaging && $(MAKE)
|
||||
|
||||
version:
|
||||
VERSION=$$(awk '/^version/{print $$3}' pyproject.toml); \
|
||||
@ -45,12 +42,11 @@ clean:
|
||||
[ -d $(DISTDIR) ] && rm -rf $(DISTDIR) || true
|
||||
[ -d $(DOCS_BUILDDIR) ] && rm -rf $(DOCS_BUILDDIR) || true
|
||||
find . -type d -name __pycache__ -exec rm -rf {} \; > /dev/null 2>&1 || true
|
||||
cd packaging/debian && $(MAKE) clean
|
||||
cd packaging/archlinux && $(MAKE) clean
|
||||
cd packaging && $(MAKE) clean
|
||||
|
||||
test-build: build debian
|
||||
scp packaging/debian/build/compute*.deb vm:~
|
||||
test-build: build-deb
|
||||
scp packaging/build/compute*.deb vm:~
|
||||
|
||||
upload-docs:
|
||||
ssh root@hitomi 'rm -rf /srv/http/nixhacks.net/hstack/compute/*'
|
||||
scp -r $(DOCS_BUILDDIR)/* root@hitomi:/srv/http/nixhacks.net/hstack/compute/
|
||||
upload-docs: docs-versions
|
||||
ssh root@hitomi 'rm -rf /srv/http/nixhacks.net/hstack/*'
|
||||
scp -r $(DOCS_BUILDDIR)/* root@hitomi:/srv/http/nixhacks.net/hstack/
|
||||
|
10
README.md
10
README.md
@ -4,8 +4,7 @@ Compute instances management library.
|
||||
|
||||
## Docs
|
||||
|
||||
Documantation is available [here](https://nixhacks.net/hstack/compute/master/index.html).
|
||||
To build actual docs run `make serve-docs`. See [Development](#development) below.
|
||||
Run `make serve-docs`. See [Development](#development) below.
|
||||
|
||||
## Roadmap
|
||||
|
||||
@ -42,7 +41,6 @@ To build actual docs run `make serve-docs`. See [Development](#development) belo
|
||||
- [ ] LXC
|
||||
- [ ] Attaching CDROM from sources: block, (http|https|ftp|ftps|tftp)://
|
||||
- [ ] Instance clones (thin, fat)
|
||||
- [ ] MicroVM
|
||||
|
||||
## Development
|
||||
|
||||
@ -54,7 +52,7 @@ Install [poetry](https://python-poetry.org/), clone this repository and run:
|
||||
poetry install --with dev --with docs
|
||||
```
|
||||
|
||||
## Build Debian package
|
||||
# Build Debian package
|
||||
|
||||
Install Docker first, then run:
|
||||
|
||||
@ -64,11 +62,11 @@ make build-deb
|
||||
|
||||
`compute` and `compute-doc` packages will built. See packaging/build directory.
|
||||
|
||||
## Installation
|
||||
# Installation
|
||||
|
||||
See [Installation](https://nixhacks.net/hstack/compute/master/installation.html).
|
||||
|
||||
## Basic usage
|
||||
# Basic usage
|
||||
|
||||
To get help run:
|
||||
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
"""Compute instances management library."""
|
||||
|
||||
__version__ = '0.1.0-dev5'
|
||||
__version__ = '0.1.0-dev3'
|
||||
|
||||
from .config import Config
|
||||
from .instance import CloudInit, Instance, InstanceConfig, InstanceSchema
|
||||
|
@ -55,7 +55,7 @@ def init(session: Session, args: argparse.Namespace) -> None:
|
||||
capabilities = session.get_capabilities()
|
||||
node_info = session.get_node_info()
|
||||
base_instance_config = {
|
||||
'name': str(uuid.uuid4()).split('-')[0],
|
||||
'name': str(uuid.uuid4()),
|
||||
'title': None,
|
||||
'description': None,
|
||||
'arch': capabilities.arch,
|
||||
@ -70,28 +70,16 @@ def init(session: Session, args: argparse.Namespace) -> None:
|
||||
'topology': None,
|
||||
'features': None,
|
||||
},
|
||||
'network_interfaces': [
|
||||
{
|
||||
'source': 'default',
|
||||
'mac': ids.random_mac(),
|
||||
},
|
||||
],
|
||||
'boot': {'order': ['cdrom', 'hd']},
|
||||
'cloud_init': None,
|
||||
}
|
||||
data = dictutil.override(base_instance_config, data)
|
||||
net_default_interface = {
|
||||
'model': 'virtio',
|
||||
'source': 'default',
|
||||
'mac': ids.random_mac(),
|
||||
}
|
||||
net_config = data.get('network', 'DEFAULT')
|
||||
if net_config == 'DEFAULT' or net_config is True:
|
||||
data['network'] = {'interfaces': [net_default_interface]}
|
||||
elif net_config is None or net_config is False:
|
||||
pass # allow creating instance without network interfaces
|
||||
else:
|
||||
interfaces = data['network'].get('interfaces')
|
||||
if interfaces:
|
||||
interfaces_configs = [
|
||||
dictutil.override(net_default_interface, interface)
|
||||
for interface in interfaces
|
||||
]
|
||||
data['network']['interfaces'] = interfaces_configs
|
||||
volumes = []
|
||||
targets = []
|
||||
for volume in data['volumes']:
|
||||
@ -258,8 +246,8 @@ def shutdown(session: Session, args: argparse.Namespace) -> None:
|
||||
method = 'SOFT'
|
||||
elif args.hard:
|
||||
method = 'HARD'
|
||||
elif args.destroy:
|
||||
method = 'DESTROY'
|
||||
elif args.unsafe:
|
||||
method = 'UNSAFE'
|
||||
else:
|
||||
method = 'NORMAL'
|
||||
instance.shutdown(method)
|
||||
|
@ -226,7 +226,7 @@ def get_parser() -> argparse.ArgumentParser:
|
||||
'-s',
|
||||
'--soft',
|
||||
action='store_true',
|
||||
help='guest OS shutdown using guest agent',
|
||||
help='normal guest OS shutdown, guest agent is used',
|
||||
)
|
||||
shutdown_opts.add_argument(
|
||||
'-n',
|
||||
@ -244,12 +244,12 @@ def get_parser() -> argparse.ArgumentParser:
|
||||
),
|
||||
)
|
||||
shutdown_opts.add_argument(
|
||||
'-d',
|
||||
'--destroy',
|
||||
'-u',
|
||||
'--unsafe',
|
||||
action='store_true',
|
||||
help=(
|
||||
'destroy instance, this is similar to a power outage '
|
||||
'and may result in data corruption'
|
||||
'and may result in data loss or corruption'
|
||||
),
|
||||
)
|
||||
shutdown.set_defaults(func=commands.shutdown)
|
||||
|
@ -92,7 +92,7 @@ class InvalidDeviceConfigError(ComputeError):
|
||||
"""Initialise InvalidDeviceConfigError."""
|
||||
self.msg = f'Invalid device XML config: {msg}'
|
||||
self.loc = f' {xml}'
|
||||
super().__init__(f'{self.msg}:\n{self.loc}')
|
||||
super().__init__(f'{self.msg}\n:{self.loc}')
|
||||
|
||||
|
||||
class InvalidDataUnitError(ValueError, ComputeError):
|
||||
|
@ -137,7 +137,7 @@ class CloudInit:
|
||||
subprocess.run(
|
||||
['/usr/sbin/mkfs.vfat', '-n', 'CIDATA', '-C', str(disk), '1024'],
|
||||
check=True,
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
self._write_to_disk(
|
||||
disk=disk,
|
||||
@ -206,7 +206,6 @@ class CloudInit:
|
||||
Attach cloud-init disk to instance.
|
||||
|
||||
:param disk: Path to disk.
|
||||
:param target: Disk target name e.g. `vda`.
|
||||
:param instance: Compute instance object.
|
||||
"""
|
||||
instance.attach_device(
|
||||
|
@ -92,8 +92,6 @@ class DiskConfig(DeviceConfig):
|
||||
encoding='unicode',
|
||||
pretty_print=True,
|
||||
).strip()
|
||||
source = xml.find('source')
|
||||
target = xml.find('target')
|
||||
driver = xml.find('driver')
|
||||
cachetype = driver.get('cache')
|
||||
disk_params = {
|
||||
@ -104,14 +102,14 @@ class DiskConfig(DeviceConfig):
|
||||
type=driver.get('type'),
|
||||
**({'cache': cachetype} if cachetype else {}),
|
||||
),
|
||||
'source': source.get('file') if source is not None else None,
|
||||
'target': target.get('dev') if target is not None else None,
|
||||
'bus': target.get('bus') if target is not None else None,
|
||||
'source': xml.find('source').get('file'),
|
||||
'target': xml.find('target').get('dev'),
|
||||
'bus': xml.find('target').get('bus'),
|
||||
'is_readonly': False if xml.find('readonly') is None else True,
|
||||
}
|
||||
for param in disk_params:
|
||||
if disk_params[param] is None:
|
||||
msg = f"missing tag '{param}'"
|
||||
msg = f"missing XML tag '{param}'"
|
||||
raise InvalidDeviceConfigError(msg, xml_str)
|
||||
if param == 'driver':
|
||||
driver = disk_params[param]
|
||||
|
@ -20,7 +20,6 @@ __all__ = ['Instance', 'InstanceConfig', 'InstanceInfo']
|
||||
import logging
|
||||
import time
|
||||
from typing import NamedTuple
|
||||
from uuid import UUID
|
||||
|
||||
import libvirt
|
||||
from lxml import etree
|
||||
@ -67,7 +66,7 @@ class InstanceConfig(EntityConfig):
|
||||
self.emulator = schema.emulator
|
||||
self.arch = schema.arch
|
||||
self.boot = schema.boot
|
||||
self.network = schema.network
|
||||
self.network_interfaces = schema.network_interfaces
|
||||
|
||||
def _gen_cpu_xml(self, cpu: CPUSchema) -> etree.Element:
|
||||
options = {
|
||||
@ -120,7 +119,6 @@ class InstanceConfig(EntityConfig):
|
||||
return E.interface(
|
||||
E.source(network=interface.source),
|
||||
E.mac(address=interface.mac),
|
||||
E.model(type=interface.model),
|
||||
type='network',
|
||||
)
|
||||
|
||||
@ -167,9 +165,8 @@ class InstanceConfig(EntityConfig):
|
||||
)
|
||||
devices = E.devices()
|
||||
devices.append(E.emulator(str(self.emulator)))
|
||||
if self.network:
|
||||
for interface in self.network.interfaces:
|
||||
devices.append(self._gen_network_interface_xml(interface))
|
||||
for interface in self.network_interfaces:
|
||||
devices.append(self._gen_network_interface_xml(interface))
|
||||
devices.append(E.graphics(type='vnc', autoport='yes'))
|
||||
devices.append(E.input(type='tablet', bus='usb'))
|
||||
devices.append(
|
||||
@ -215,14 +212,18 @@ class Instance:
|
||||
|
||||
def __init__(self, domain: libvirt.virDomain):
|
||||
"""
|
||||
Initialise Compute Instance object.
|
||||
Initialise Instance.
|
||||
|
||||
:ivar libvirt.virDomain domain: domain object
|
||||
:ivar libvirt.virConnect connection: connection object
|
||||
:ivar str name: domain name
|
||||
:ivar GuestAgent guest_agent: :class:`GuestAgent` object
|
||||
|
||||
:param domain: libvirt domain object
|
||||
"""
|
||||
self._domain = domain
|
||||
self._connection = domain.connect()
|
||||
self._name = domain.name()
|
||||
self._uuid = domain.UUID()
|
||||
self._guest_agent = GuestAgent(domain)
|
||||
|
||||
@property
|
||||
@ -240,11 +241,6 @@ class Instance:
|
||||
"""Instance name."""
|
||||
return self._name
|
||||
|
||||
@property
|
||||
def uuid(self) -> UUID:
|
||||
"""Instance UUID."""
|
||||
return UUID(bytes=self._uuid)
|
||||
|
||||
@property
|
||||
def guest_agent(self) -> GuestAgent:
|
||||
""":class:`GuestAgent` object."""
|
||||
@ -291,9 +287,10 @@ class Instance:
|
||||
|
||||
def is_running(self) -> bool:
|
||||
"""Return True if instance is running, else return False."""
|
||||
if self.domain.isActive() == 1:
|
||||
return True
|
||||
return False
|
||||
if self.domain.isActive() != 1:
|
||||
# 0 - is inactive, -1 - is error
|
||||
return False
|
||||
return True
|
||||
|
||||
def is_autostart(self) -> bool:
|
||||
"""Return True if instance autostart is enabled, else return False."""
|
||||
@ -321,7 +318,7 @@ class Instance:
|
||||
log.info("Starting instance '%s'", self.name)
|
||||
if self.is_running():
|
||||
log.warning(
|
||||
"Instance '%s' is already started, nothing to do", self.name
|
||||
'Already started, nothing to do instance=%s', self.name
|
||||
)
|
||||
return
|
||||
try:
|
||||
@ -350,8 +347,8 @@ class Instance:
|
||||
to unplugging machine from power. Internally send SIGTERM to
|
||||
instance process and destroy it gracefully.
|
||||
|
||||
DESTROY
|
||||
Forced shutdown. Internally send SIGKILL to instance process.
|
||||
UNSAFE
|
||||
Force shutdown. Internally send SIGKILL to instance process.
|
||||
There is high data corruption risk!
|
||||
|
||||
If method is None NORMAL method will used.
|
||||
@ -364,7 +361,7 @@ class Instance:
|
||||
'SOFT': libvirt.VIR_DOMAIN_SHUTDOWN_GUEST_AGENT,
|
||||
'NORMAL': libvirt.VIR_DOMAIN_SHUTDOWN_DEFAULT,
|
||||
'HARD': libvirt.VIR_DOMAIN_DESTROY_GRACEFUL,
|
||||
'DESTROY': libvirt.VIR_DOMAIN_DESTROY_DEFAULT,
|
||||
'UNSAFE': libvirt.VIR_DOMAIN_DESTROY_DEFAULT,
|
||||
}
|
||||
if method is None:
|
||||
method = 'NORMAL'
|
||||
@ -375,13 +372,11 @@ class Instance:
|
||||
method = method.upper()
|
||||
if method not in methods:
|
||||
raise ValueError(f"Unsupported shutdown method: '{method}'")
|
||||
if method == 'SOFT' and self.guest_agent.is_available() is False:
|
||||
method = 'NORMAL'
|
||||
log.info("Performing instance shutdown with method '%s'", method)
|
||||
try:
|
||||
if method in ['SOFT', 'NORMAL']:
|
||||
self.domain.shutdownFlags(flags=methods[method])
|
||||
elif method in ['HARD', 'DESTROY']:
|
||||
elif method in ['HARD', 'UNSAFE']:
|
||||
self.domain.destroyFlags(flags=methods[method])
|
||||
except libvirt.libvirtError as e:
|
||||
raise InstanceError(
|
||||
@ -448,7 +443,8 @@ class Instance:
|
||||
self.domain.setAutostart(autostart)
|
||||
except libvirt.libvirtError as e:
|
||||
raise InstanceError(
|
||||
f"Cannot set {autostart=} flag for instance '{self.name}': {e}"
|
||||
f'Cannot set autostart flag for instance={self.name} '
|
||||
f'{autostart=}: {e}'
|
||||
) from e
|
||||
|
||||
def set_vcpus(self, nvcpus: int, *, live: bool = False) -> None:
|
||||
@ -470,7 +466,7 @@ class Instance:
|
||||
raise InstanceError('vCPUs count is greather than max_vcpus')
|
||||
if nvcpus == self.get_info().nproc:
|
||||
log.warning(
|
||||
"Instance '%s' already have %s vCPUs, nothing to do",
|
||||
'Instance instance=%s already have %s vCPUs, nothing to do',
|
||||
self.name,
|
||||
nvcpus,
|
||||
)
|
||||
@ -496,17 +492,18 @@ class Instance:
|
||||
self.domain.setVcpusFlags(nvcpus, flags=flags)
|
||||
except GuestAgentCommandNotSupportedError:
|
||||
log.warning(
|
||||
"'guest-set-vcpus' command is not supported, '"
|
||||
'you may need to enable CPUs in guest manually.'
|
||||
'Cannot set vCPUs in guest via agent, you may '
|
||||
'need to apply changes in guest manually.'
|
||||
)
|
||||
else:
|
||||
log.warning(
|
||||
'Guest agent is not installed or not connected, '
|
||||
'you may need to enable CPUs in guest manually.'
|
||||
'Cannot set vCPUs in guest OS on instance=%s. '
|
||||
'You may need to apply CPUs in guest manually.',
|
||||
self.name,
|
||||
)
|
||||
except libvirt.libvirtError as e:
|
||||
raise InstanceError(
|
||||
f"Cannot set vCPUs for instance '{self.name}': {e}"
|
||||
f'Cannot set vCPUs for instance={self.name}: {e}'
|
||||
) from e
|
||||
|
||||
def set_memory(self, memory: int, *, live: bool = False) -> None:
|
||||
@ -788,12 +785,6 @@ class Instance:
|
||||
disk.source,
|
||||
)
|
||||
continue
|
||||
if volume.storagePoolLookupByVolume().name() == 'images':
|
||||
log.info(
|
||||
'Volume %s skipped because it is from images pool',
|
||||
volume.path(),
|
||||
)
|
||||
continue
|
||||
log.info('Delete volume: %s', volume.path())
|
||||
volume.delete()
|
||||
log.info('Undefine instance')
|
||||
|
@ -16,11 +16,11 @@
|
||||
"""Compute instance related objects schemas."""
|
||||
|
||||
import re
|
||||
from collections import Counter
|
||||
from enum import StrEnum
|
||||
from pathlib import Path
|
||||
|
||||
from pydantic import validator
|
||||
from pydantic import ValidationError, validator
|
||||
from pydantic.error_wrappers import ErrorWrapper
|
||||
|
||||
from compute.abstract import EntityModel
|
||||
from compute.utils.units import DataUnit
|
||||
@ -74,30 +74,12 @@ class VolumeCapacitySchema(EntityModel):
|
||||
unit: DataUnit
|
||||
|
||||
|
||||
class DiskCache(StrEnum):
|
||||
"""Possible disk cache mechanisms enumeration."""
|
||||
|
||||
NONE = 'none'
|
||||
WRITETHROUGH = 'writethrough'
|
||||
WRITEBACK = 'writeback'
|
||||
DIRECTSYNC = 'directsync'
|
||||
UNSAFE = 'unsafe'
|
||||
|
||||
|
||||
class DiskDriverSchema(EntityModel):
|
||||
"""Virtual disk driver model."""
|
||||
|
||||
name: str
|
||||
type: str # noqa: A003
|
||||
cache: DiskCache = DiskCache.WRITETHROUGH
|
||||
|
||||
|
||||
class DiskBus(StrEnum):
|
||||
"""Possible disk buses enumeration."""
|
||||
|
||||
VIRTIO = 'virtio'
|
||||
IDE = 'ide'
|
||||
SATA = 'sata'
|
||||
cache: str = 'writethrough'
|
||||
|
||||
|
||||
class VolumeSchema(EntityModel):
|
||||
@ -110,30 +92,15 @@ class VolumeSchema(EntityModel):
|
||||
source: str | None = None
|
||||
is_readonly: bool = False
|
||||
is_system: bool = False
|
||||
bus: DiskBus = DiskBus.VIRTIO
|
||||
bus: str = 'virtio'
|
||||
device: str = 'disk'
|
||||
|
||||
|
||||
class NetworkAdapterModel(StrEnum):
|
||||
"""Network adapter models."""
|
||||
|
||||
VIRTIO = 'virtio'
|
||||
E1000 = 'e1000'
|
||||
RTL8139 = 'rtl8139'
|
||||
|
||||
|
||||
class NetworkInterfaceSchema(EntityModel):
|
||||
"""Network inerface model."""
|
||||
|
||||
source: str
|
||||
mac: str
|
||||
model: NetworkAdapterModel
|
||||
|
||||
|
||||
class NetworkSchema(EntityModel):
|
||||
"""Network configuration schema."""
|
||||
|
||||
interfaces: list[NetworkInterfaceSchema]
|
||||
|
||||
|
||||
class BootOptionsSchema(EntityModel):
|
||||
@ -167,7 +134,7 @@ class InstanceSchema(EntityModel):
|
||||
arch: str
|
||||
boot: BootOptionsSchema
|
||||
volumes: list[VolumeSchema]
|
||||
network: NetworkSchema | None | bool
|
||||
network_interfaces: list[NetworkInterfaceSchema]
|
||||
image: str | None = None
|
||||
cloud_init: CloudInitSchema | None = None
|
||||
|
||||
@ -175,7 +142,7 @@ class InstanceSchema(EntityModel):
|
||||
def _check_name(cls, value: str) -> str: # noqa: N805
|
||||
if not re.match(r'^[a-z0-9_-]+$', value):
|
||||
msg = (
|
||||
'Name must contain only lowercase letters, numbers, '
|
||||
'Name can contain only lowercase letters, numbers, '
|
||||
'minus sign and underscore.'
|
||||
)
|
||||
raise ValueError(msg)
|
||||
@ -195,33 +162,27 @@ class InstanceSchema(EntityModel):
|
||||
if len([v for v in volumes if v.is_system is True]) != 1:
|
||||
msg = 'volumes list must contain one system volume'
|
||||
raise ValueError(msg)
|
||||
index = 0
|
||||
for volume in volumes:
|
||||
index += 1
|
||||
if volume.source is None and volume.capacity is None:
|
||||
msg = f"{index}: capacity is required if 'source' is unset"
|
||||
raise ValueError(msg)
|
||||
if volume.is_system is True and volume.is_readonly is True:
|
||||
for vol in volumes:
|
||||
if vol.source is None and vol.capacity is None:
|
||||
raise ValidationError(
|
||||
[
|
||||
ErrorWrapper(
|
||||
Exception(
|
||||
"capacity is required if 'source' is unset"
|
||||
),
|
||||
loc='X.capacity',
|
||||
)
|
||||
],
|
||||
model=VolumeSchema,
|
||||
)
|
||||
if vol.is_system is True and vol.is_readonly is True:
|
||||
msg = 'volume marked as system cannot be readonly'
|
||||
raise ValueError(msg)
|
||||
sources = [v.source for v in volumes if v.source is not None]
|
||||
targets = [v.target for v in volumes]
|
||||
for item in [sources, targets]:
|
||||
duplicates = Counter(item) - Counter(set(item))
|
||||
if duplicates:
|
||||
msg = f'find duplicate values: {list(duplicates)}'
|
||||
raise ValueError(msg)
|
||||
return volumes
|
||||
|
||||
@validator('network')
|
||||
def _check_network(
|
||||
cls, # noqa: N805
|
||||
network: NetworkSchema | None | bool,
|
||||
) -> NetworkSchema | None | bool:
|
||||
if network is True:
|
||||
msg = (
|
||||
"'network' cannot be True, set it to False "
|
||||
'or provide network configuration'
|
||||
)
|
||||
@validator('network_interfaces')
|
||||
def _check_network_interfaces(cls, value: list) -> list: # noqa: N805
|
||||
if not value:
|
||||
msg = 'Network interfaces list must contain at least one element'
|
||||
raise ValueError(msg)
|
||||
return network
|
||||
return value
|
||||
|
@ -17,7 +17,6 @@
|
||||
|
||||
import logging
|
||||
from contextlib import AbstractContextManager
|
||||
from pathlib import Path
|
||||
from types import TracebackType
|
||||
from typing import Any, NamedTuple
|
||||
from uuid import uuid4
|
||||
@ -208,12 +207,9 @@ class Session(AbstractContextManager):
|
||||
:param volumes: List of storage volume configs. For more info
|
||||
see :class:`VolumeSchema`.
|
||||
:type volumes: list[dict]
|
||||
:param network: List of virtual network interfaces configs.
|
||||
See :class:`NetworkSchema` for more info.
|
||||
:param network_interfaces: List of virtual network interfaces
|
||||
configs. See :class:`NetworkInterfaceSchema` for more info.
|
||||
:type network_interfaces: list[dict]
|
||||
:param cloud_init: Cloud-init configuration. See
|
||||
:class:`CloudInitSchema` for info.
|
||||
:type cloud_init: dict
|
||||
"""
|
||||
data = InstanceSchema(**kwargs)
|
||||
config = InstanceConfig(data)
|
||||
@ -246,17 +242,11 @@ class Session(AbstractContextManager):
|
||||
log.info('Volume %s is CDROM device', volume_name)
|
||||
elif volume.source is not None:
|
||||
log.info('Using volume %s as source', volume_name)
|
||||
volume_source = volume.source
|
||||
if volume.capacity:
|
||||
capacity = units.to_bytes(
|
||||
volume.capacity.value, volume.capacity.unit
|
||||
)
|
||||
log.info('Getting volume %s', volume.source)
|
||||
vol = volumes_pool.get_volume(Path(volume_name).name)
|
||||
log.info(
|
||||
'Resize volume to specified size: %s',
|
||||
capacity,
|
||||
)
|
||||
vol.resize(capacity, unit=units.DataUnit.BYTES)
|
||||
else:
|
||||
capacity = units.to_bytes(
|
||||
volume.capacity.value, volume.capacity.unit
|
||||
@ -266,7 +256,7 @@ class Session(AbstractContextManager):
|
||||
path=str(volumes_pool.path.joinpath(volume_name)),
|
||||
capacity=capacity,
|
||||
)
|
||||
volume.source = volume_config.path
|
||||
volume_source = volume_config.path
|
||||
log.debug('Volume config: %s', volume_config)
|
||||
if volume.is_system is True and data.image:
|
||||
log.info(
|
||||
@ -276,20 +266,21 @@ class Session(AbstractContextManager):
|
||||
image = images_pool.get_volume(data.image)
|
||||
log.info('Cloning image into volumes pool...')
|
||||
vol = volumes_pool.clone_volume(image, volume_config)
|
||||
log.info(
|
||||
'Resize cloned volume to specified size: %s',
|
||||
capacity,
|
||||
)
|
||||
vol.resize(capacity, unit=units.DataUnit.BYTES)
|
||||
else:
|
||||
log.info('Create volume %s', volume_config.name)
|
||||
volumes_pool.create_volume(volume_config)
|
||||
if capacity is not None:
|
||||
log.info(
|
||||
'Resize cloned volume to specified size: %s',
|
||||
capacity,
|
||||
)
|
||||
vol.resize(capacity, unit=units.DataUnit.BYTES)
|
||||
log.info('Attaching volume to instance...')
|
||||
instance.attach_device(
|
||||
DiskConfig(
|
||||
type=volume.type,
|
||||
device=volume.device,
|
||||
source=volume.source,
|
||||
source=volume_source,
|
||||
target=volume.target,
|
||||
is_readonly=volume.is_readonly,
|
||||
bus=volume.bus,
|
||||
|
@ -73,9 +73,9 @@ class StoragePool:
|
||||
"""
|
||||
Refresh storage pool.
|
||||
|
||||
:param retry: If True retry pool refresh on 'pool have running
|
||||
asynchronous jobs' error.
|
||||
:param timeout: Retry timeout in seconds. Affects only if `retry`
|
||||
:param retry: If True retry pool refresh on :class:`libvirtError`
|
||||
with running asynchronous jobs.
|
||||
:param timeout: Retry timeout in secodns. Affets only if `retry`
|
||||
is True.
|
||||
"""
|
||||
retry_timeout = dt.now(tz=datetime.UTC) + timedelta(seconds=timeout)
|
||||
@ -119,7 +119,7 @@ class StoragePool:
|
||||
'src_pool=%s src_vol=%s dst_pool=%s dst_vol=%s',
|
||||
src.pool_name,
|
||||
src.name,
|
||||
self.pool.name(),
|
||||
self.pool.name,
|
||||
dst.name,
|
||||
)
|
||||
vol = self.pool.createXMLFrom(
|
||||
@ -134,9 +134,7 @@ class StoragePool:
|
||||
def get_volume(self, name: str) -> Volume | None:
|
||||
"""Lookup and return Volume instance or None."""
|
||||
log.info(
|
||||
'Lookup for storage volume vol=%s in pool=%s',
|
||||
name,
|
||||
self.pool.name(),
|
||||
'Lookup for storage volume vol=%s in pool=%s', name, self.pool.name
|
||||
)
|
||||
try:
|
||||
vol = self.pool.storageVolLookupByName(name)
|
||||
|
@ -15,19 +15,19 @@
|
||||
|
||||
"""Random identificators."""
|
||||
|
||||
# ruff: noqa: S311
|
||||
# ruff: noqa: S311, C417
|
||||
|
||||
import random
|
||||
|
||||
|
||||
def random_mac() -> str:
|
||||
"""Retrun random MAC address."""
|
||||
bits = [
|
||||
0x0A,
|
||||
random.randint(0x00, 0xFF),
|
||||
random.randint(0x00, 0xFF),
|
||||
random.randint(0x00, 0xFF),
|
||||
mac = [
|
||||
0x00,
|
||||
0x16,
|
||||
0x3E,
|
||||
random.randint(0x00, 0x7F),
|
||||
random.randint(0x00, 0xFF),
|
||||
random.randint(0x00, 0xFF),
|
||||
]
|
||||
return ':'.join([f'{b:02x}' for b in bits])
|
||||
return ':'.join(map(lambda x: '%02x' % x, mac))
|
||||
|
@ -15,7 +15,6 @@
|
||||
|
||||
"""Tools for data units convertion."""
|
||||
|
||||
from collections.abc import Callable
|
||||
from enum import StrEnum
|
||||
|
||||
from compute.exceptions import InvalidDataUnitError
|
||||
@ -29,14 +28,6 @@ class DataUnit(StrEnum):
|
||||
MIB = 'MiB'
|
||||
GIB = 'GiB'
|
||||
TIB = 'TiB'
|
||||
KB = 'kb'
|
||||
MB = 'Mb'
|
||||
GB = 'Gb'
|
||||
TB = 'Tb'
|
||||
KBIT = 'kbit'
|
||||
MBIT = 'Mbit'
|
||||
GBIT = 'Gbit'
|
||||
TBIT = 'Tbit'
|
||||
|
||||
@classmethod
|
||||
def _missing_(cls, name: str) -> 'DataUnit':
|
||||
@ -46,74 +37,17 @@ class DataUnit(StrEnum):
|
||||
return None
|
||||
|
||||
|
||||
def validate_input(*args: str) -> Callable:
|
||||
"""Validate data units in functions input."""
|
||||
to_validate = args
|
||||
|
||||
def decorator(func: Callable) -> Callable:
|
||||
def wrapper(*args: float | str, **kwargs: str) -> Callable:
|
||||
try:
|
||||
if kwargs:
|
||||
for arg in to_validate:
|
||||
unit = kwargs[arg]
|
||||
DataUnit(unit)
|
||||
else:
|
||||
for arg in args[1:]:
|
||||
unit = arg
|
||||
DataUnit(unit)
|
||||
except ValueError as e:
|
||||
raise InvalidDataUnitError(e, list(DataUnit)) from e
|
||||
return func(*args, **kwargs)
|
||||
|
||||
return wrapper
|
||||
|
||||
return decorator
|
||||
|
||||
|
||||
@validate_input('unit')
|
||||
def to_bytes(value: float, unit: DataUnit = DataUnit.BYTES) -> float:
|
||||
"""Convert value to bytes."""
|
||||
unit = DataUnit(unit)
|
||||
basis = 2 if unit.endswith('iB') else 10
|
||||
factor = 125 if unit.endswith('bit') else 1
|
||||
power = {
|
||||
def to_bytes(value: int, unit: DataUnit = DataUnit.BYTES) -> int:
|
||||
"""Convert value to bytes. See :class:`DataUnit`."""
|
||||
try:
|
||||
_ = DataUnit(unit)
|
||||
except ValueError as e:
|
||||
raise InvalidDataUnitError(e, list(DataUnit)) from e
|
||||
powers = {
|
||||
DataUnit.BYTES: 0,
|
||||
DataUnit.KIB: 10,
|
||||
DataUnit.MIB: 20,
|
||||
DataUnit.GIB: 30,
|
||||
DataUnit.TIB: 40,
|
||||
DataUnit.KB: 3,
|
||||
DataUnit.MB: 6,
|
||||
DataUnit.GB: 9,
|
||||
DataUnit.TB: 12,
|
||||
DataUnit.KBIT: 0,
|
||||
DataUnit.MBIT: 3,
|
||||
DataUnit.GBIT: 6,
|
||||
DataUnit.TBIT: 9,
|
||||
DataUnit.KIB: 1,
|
||||
DataUnit.MIB: 2,
|
||||
DataUnit.GIB: 3,
|
||||
DataUnit.TIB: 4,
|
||||
}
|
||||
return value * factor * pow(basis, power[unit])
|
||||
|
||||
|
||||
@validate_input('from_unit', 'to_unit')
|
||||
def convert(value: float, from_unit: DataUnit, to_unit: DataUnit) -> float:
|
||||
"""Convert units."""
|
||||
value_in_bits = to_bytes(value, from_unit) * 8
|
||||
to_unit = DataUnit(to_unit)
|
||||
basis = 2 if to_unit.endswith('iB') else 10
|
||||
divisor = 1 if to_unit.endswith('bit') else 8
|
||||
power = {
|
||||
DataUnit.BYTES: 0,
|
||||
DataUnit.KIB: 10,
|
||||
DataUnit.MIB: 20,
|
||||
DataUnit.GIB: 30,
|
||||
DataUnit.TIB: 40,
|
||||
DataUnit.KB: 3,
|
||||
DataUnit.MB: 6,
|
||||
DataUnit.GB: 9,
|
||||
DataUnit.TB: 12,
|
||||
DataUnit.KBIT: 3,
|
||||
DataUnit.MBIT: 6,
|
||||
DataUnit.GBIT: 9,
|
||||
DataUnit.TBIT: 12,
|
||||
}
|
||||
return value_in_bits / divisor / pow(basis, power[to_unit])
|
||||
return value * pow(1024, powers[unit])
|
||||
|
@ -27,6 +27,9 @@ Cloud-init configs may be set inplace into :file:`instance.yaml`.
|
||||
user_data: |
|
||||
## template: jinja
|
||||
#cloud-config
|
||||
merge_how:
|
||||
- name: list
|
||||
settings: [append]
|
||||
hostname: {{ ds.meta_data.hostname }}
|
||||
fqdn: {{ ds.meta_data.hostname }}.instances.generic.cloud
|
||||
manage_etc_hosts: true
|
||||
|
@ -6,7 +6,7 @@ sys.path.insert(0, os.path.abspath('../..'))
|
||||
project = 'Compute'
|
||||
copyright = '2023, Compute Authors'
|
||||
author = 'Compute Authors'
|
||||
release = '0.1.0-dev5'
|
||||
release = '0.1.0-dev3'
|
||||
|
||||
# Sphinx general settings
|
||||
extensions = [
|
||||
|
@ -127,11 +127,9 @@ boot:
|
||||
- hd
|
||||
# Network configuration. This decision is temporary and will be changed in
|
||||
# the future. We recommend not using this option.
|
||||
network:
|
||||
interfaces:
|
||||
- mac: 00:16:3e:7e:8c:4a
|
||||
source: default
|
||||
model: virtio
|
||||
network_interfaces:
|
||||
- mac: 00:16:3e:7e:8c:4a
|
||||
source: default
|
||||
# Disk image
|
||||
image: /images/debian-12-generic-amd64.qcow2
|
||||
# Storage volumes list
|
||||
|
24
packaging/Makefile
Normal file
24
packaging/Makefile
Normal file
@ -0,0 +1,24 @@
|
||||
DOCKER_CMD ?= docker
|
||||
DOCKER_IMG = pybuilder:bookworm
|
||||
DEBBUILDDIR = build
|
||||
|
||||
all: docker-build build
|
||||
|
||||
clean:
|
||||
test -d $(DEBBUILDDIR) && rm -rf $(DEBBUILDDIR) || true
|
||||
|
||||
docker-build:
|
||||
$(DOCKER_CMD) build -f Dockerfile -t $(DOCKER_IMG) .
|
||||
|
||||
build: clean
|
||||
mkdir -p $(DEBBUILDDIR)
|
||||
cp -v ../dist/compute-*[.tar.gz] $(DEBBUILDDIR)/
|
||||
cp -r ../docs $(DEBBUILDDIR)/
|
||||
if [ -f build.sh.bak ]; then mv build.sh{.bak,}; fi
|
||||
cp build.sh{,.bak}
|
||||
awk '/authors/{gsub(/[\[\]]/,"");print $$3" "$$4}' ../pyproject.toml \
|
||||
| sed "s/['<>]//g" \
|
||||
| tr ' ' '\n' \
|
||||
| xargs -I {} sed "0,/%placeholder%/s//{}/" -i build.sh
|
||||
$(DOCKER_CMD) run --rm -i -v $$PWD:/mnt $(DOCKER_IMG) bash < build.sh
|
||||
mv build.sh{.bak,}
|
@ -1,10 +0,0 @@
|
||||
FROM archlinux:latest
|
||||
WORKDIR /mnt
|
||||
RUN chown 1000:1000 /mnt; \
|
||||
pacman -Sy --noconfirm \
|
||||
fakeroot \
|
||||
binutils \
|
||||
python \
|
||||
python-pip; \
|
||||
echo "alias ll='ls -alFh'" >> /etc/bash.bashrc
|
||||
USER 1000:1000
|
@ -1,22 +0,0 @@
|
||||
DOCKER_CMD ?= docker
|
||||
DOCKER_IMG = computebuilder:archlinux
|
||||
BUILDDIR = build
|
||||
|
||||
all: docker-build build
|
||||
|
||||
clean:
|
||||
test -d $(BUILDDIR) && rm -rf $(BUILDDIR) || true
|
||||
|
||||
docker-build:
|
||||
$(DOCKER_CMD) build -f Dockerfile -t $(DOCKER_IMG) .
|
||||
|
||||
build: clean
|
||||
mkdir -p $(BUILDDIR)
|
||||
VERSION=$$(awk '/^version/{print $$3}' ../../pyproject.toml | sed s'/-/\./'); \
|
||||
sed "s/pkgver=.*/pkgver=$$VERSION/" PKGBUILD > $(BUILDDIR)/PKGBUILD
|
||||
cp -v ../../dist/compute-*[.tar.gz] $(BUILDDIR)/
|
||||
cp ../../extra/completion.bash $(BUILDDIR)/
|
||||
$(DOCKER_CMD) run --rm -i -v $$PWD/$(BUILDDIR):/mnt --ulimit "nofile=1024:1048576" \
|
||||
$(DOCKER_IMG) makepkg --nodeps --clean
|
||||
# Remove unwanted files from build dir
|
||||
find $(BUILDDIR) ! -name '*.pkg.tar.zst' -type f -exec rm -f {} +
|
@ -1,21 +0,0 @@
|
||||
pkgname=compute
|
||||
pkgver='%placeholder%'
|
||||
pkgrel=1
|
||||
pkgdesc='Compute instances management library'
|
||||
arch=(any)
|
||||
url=https://get.lulzette.ru/hstack/compute
|
||||
license=('GPL-3-or-later')
|
||||
makedepends=(python python-pip)
|
||||
depends=(python libvirt libvirt-python qemu-base qemu-system-x86 qemu-img)
|
||||
optdepends=(
|
||||
'dnsmasq: required for default NAT/DHCP'
|
||||
'iptables-nft: required for default NAT'
|
||||
)
|
||||
provides=(compute)
|
||||
conflicts=()
|
||||
|
||||
package() {
|
||||
pip install --no-cache-dir --no-deps --root $pkgdir ../$pkgname-*.tar.gz
|
||||
install -Dm644 ../completion.bash $pkgdir/usr/share/bash-completion/completions/compute
|
||||
install -Dm644 $pkgdir/usr/lib/*/site-packages/computed.toml $pkgdir/etc/compute/computed.toml
|
||||
}
|
@ -11,6 +11,5 @@ sed -e "s%\.\./\.\.%$PWD%" -i ../docs/source/conf.py
|
||||
dh_make --copyright gpl3 --yes --python --file ../compute-*[.tar.gz]
|
||||
rm debian/*.ex debian/README.{Debian,source} debian/*.docs
|
||||
sed -e 's/\* Initial release.*/\* This is the development build, see commits in upstream repo for info./' -i debian/changelog
|
||||
cp -v ../../files/{control,rules,copyright,docs,install} debian/
|
||||
mv ../compute.bash-completion debian/
|
||||
cp -v ../../files/{control,rules,copyright,docs,compute.bash-completion,install} debian/
|
||||
dpkg-buildpackage -us -uc
|
@ -1,29 +0,0 @@
|
||||
DOCKER_CMD ?= docker
|
||||
DOCKER_IMG = computebuilder:debian-bookworm
|
||||
BUILDDIR = build
|
||||
KEEP_BUILDFILES ?=
|
||||
|
||||
all: docker-build build
|
||||
|
||||
clean:
|
||||
test -d $(BUILDDIR) && rm -rf $(BUILDDIR) || true
|
||||
|
||||
docker-build:
|
||||
$(DOCKER_CMD) build -f Dockerfile -t $(DOCKER_IMG) .
|
||||
|
||||
build: clean
|
||||
mkdir -p $(BUILDDIR)
|
||||
cp -v ../../dist/compute-*[.tar.gz] $(BUILDDIR)/
|
||||
cp -r ../../docs $(BUILDDIR)/
|
||||
cp ../../extra/completion.bash $(BUILDDIR)/compute.bash-completion
|
||||
if [ -f build.sh.bak ]; then mv build.sh.bak build.sh; fi
|
||||
cp build.sh{,.bak}
|
||||
awk '/authors/{gsub(/[\[\]]/,"");print $$3" "$$4}' ../pyproject.toml \
|
||||
| sed "s/['<>]//g" \
|
||||
| tr ' ' '\n' \
|
||||
| xargs -I {} sed "0,/%placeholder%/s//{}/" -i build.sh
|
||||
$(DOCKER_CMD) run --rm -i -v $$PWD:/mnt $(DOCKER_IMG) bash < build.sh
|
||||
mv build.sh{.bak,}
|
||||
# Remove unwanted files from build dir
|
||||
find $(BUILDDIR) -mindepth 1 -type d -exec rm -rf {} +
|
||||
[ -z $(KEEP_BUILDFILES) ] && find $(BUILDDIR) ! -name '*.deb' -type f -exec rm -f {} + || true
|
@ -28,19 +28,16 @@ Depends:
|
||||
${misc:Depends},
|
||||
qemu-system,
|
||||
qemu-utils,
|
||||
libvirt-daemon,
|
||||
libvirt-daemon-system,
|
||||
libvirt-daemon-driver-qemu,
|
||||
libvirt-clients,
|
||||
python3-libvirt,
|
||||
python3-lxml,
|
||||
python3-yaml,
|
||||
python3-pydantic,
|
||||
mtools,
|
||||
dosfstools,
|
||||
dosfstools
|
||||
Recommends:
|
||||
dnsmasq,
|
||||
dnsmasq-base
|
||||
dnsmasq
|
||||
Suggests:
|
||||
compute-doc
|
||||
Description: Compute instances management library (Python 3)
|
@ -1,6 +1,6 @@
|
||||
[tool.poetry]
|
||||
name = 'compute'
|
||||
version = '0.1.0-dev5'
|
||||
version = '0.1.0-dev3'
|
||||
description = 'Compute instances management library'
|
||||
license = 'GPL-3.0-or-later'
|
||||
authors = ['ge <ge@nixhacks.net>']
|
||||
|
Loading…
x
Reference in New Issue
Block a user