Compare commits

..

No commits in common. "master" and "v0.1.0-dev3" have entirely different histories.

32 changed files with 156 additions and 363 deletions

2
.gitignore vendored
View File

@ -1,6 +1,6 @@
dist/
docs/build/
packaging/*/build/
packaging/build/
instance.yaml
.ruff_cache/
__pycache__/

View File

@ -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/

View File

@ -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:

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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):

View File

@ -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(

View File

@ -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]

View File

@ -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')

View File

@ -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

View File

@ -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,

View File

@ -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)

View File

@ -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))

View File

@ -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])

View File

@ -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

View File

@ -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 = [

View File

@ -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
View 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,}

View File

@ -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

View File

@ -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 {} +

View File

@ -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
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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>']