forked from uncloud/uncloud
Move all files to _etc_based
This commit is contained in:
parent
10f09c7115
commit
3cf3439f1c
116 changed files with 1 additions and 0 deletions
3
uncloud_etcd_based/uncloud/common/__init__.py
Normal file
3
uncloud_etcd_based/uncloud/common/__init__.py
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
26
uncloud_etcd_based/uncloud/common/classes.py
Normal file
26
uncloud_etcd_based/uncloud/common/classes.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from .etcd_wrapper import EtcdEntry
|
||||
|
||||
|
||||
class SpecificEtcdEntryBase:
|
||||
def __init__(self, e: EtcdEntry):
|
||||
self.key = e.key
|
||||
|
||||
for k in e.value.keys():
|
||||
self.__setattr__(k, e.value[k])
|
||||
|
||||
def original_keys(self):
|
||||
r = dict(self.__dict__)
|
||||
if "key" in r:
|
||||
del r["key"]
|
||||
return r
|
||||
|
||||
@property
|
||||
def value(self):
|
||||
return self.original_keys()
|
||||
|
||||
@value.setter
|
||||
def value(self, v):
|
||||
self.__dict__ = v
|
||||
|
||||
def __repr__(self):
|
||||
return str(dict(self.__dict__))
|
||||
26
uncloud_etcd_based/uncloud/common/cli.py
Normal file
26
uncloud_etcd_based/uncloud/common/cli.py
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
from uncloud.common.shared import shared
|
||||
from pyotp import TOTP
|
||||
|
||||
|
||||
def get_token(seed):
|
||||
if seed is not None:
|
||||
try:
|
||||
token = TOTP(seed).now()
|
||||
except Exception:
|
||||
raise Exception('Invalid seed')
|
||||
else:
|
||||
return token
|
||||
|
||||
|
||||
def resolve_otp_credentials(kwargs):
|
||||
d = {
|
||||
'name': shared.settings['client']['name'],
|
||||
'realm': shared.settings['client']['realm'],
|
||||
'token': get_token(shared.settings['client']['seed'])
|
||||
}
|
||||
|
||||
for k, v in d.items():
|
||||
if k in kwargs and kwargs[k] is None:
|
||||
kwargs.update({k: v})
|
||||
|
||||
return d
|
||||
21
uncloud_etcd_based/uncloud/common/counters.py
Normal file
21
uncloud_etcd_based/uncloud/common/counters.py
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
from .etcd_wrapper import Etcd3Wrapper
|
||||
|
||||
|
||||
def increment_etcd_counter(etcd_client: Etcd3Wrapper, key):
|
||||
kv = etcd_client.get(key)
|
||||
|
||||
if kv:
|
||||
counter = int(kv.value)
|
||||
counter = counter + 1
|
||||
else:
|
||||
counter = 1
|
||||
|
||||
etcd_client.put(key, str(counter))
|
||||
return counter
|
||||
|
||||
|
||||
def get_etcd_counter(etcd_client: Etcd3Wrapper, key):
|
||||
kv = etcd_client.get(key)
|
||||
if kv:
|
||||
return int(kv.value)
|
||||
return None
|
||||
75
uncloud_etcd_based/uncloud/common/etcd_wrapper.py
Normal file
75
uncloud_etcd_based/uncloud/common/etcd_wrapper.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
import etcd3
|
||||
import json
|
||||
|
||||
from functools import wraps
|
||||
|
||||
from uncloud import UncloudException
|
||||
from uncloud.common import logger
|
||||
|
||||
|
||||
class EtcdEntry:
|
||||
def __init__(self, meta_or_key, value, value_in_json=False):
|
||||
if hasattr(meta_or_key, 'key'):
|
||||
# if meta has attr 'key' then get it
|
||||
self.key = meta_or_key.key.decode('utf-8')
|
||||
else:
|
||||
# otherwise meta is the 'key'
|
||||
self.key = meta_or_key
|
||||
self.value = value.decode('utf-8')
|
||||
|
||||
if value_in_json:
|
||||
self.value = json.loads(self.value)
|
||||
|
||||
|
||||
def readable_errors(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except etcd3.exceptions.ConnectionFailedError:
|
||||
raise UncloudException('Cannot connect to etcd: is etcd running as configured in uncloud.conf?')
|
||||
except etcd3.exceptions.ConnectionTimeoutError as err:
|
||||
raise etcd3.exceptions.ConnectionTimeoutError('etcd connection timeout.') from err
|
||||
except Exception:
|
||||
logger.exception('Some etcd error occured. See syslog for details.')
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class Etcd3Wrapper:
|
||||
@readable_errors
|
||||
def __init__(self, *args, **kwargs):
|
||||
self.client = etcd3.client(*args, **kwargs)
|
||||
|
||||
@readable_errors
|
||||
def get(self, *args, value_in_json=False, **kwargs):
|
||||
_value, _key = self.client.get(*args, **kwargs)
|
||||
if _key is None or _value is None:
|
||||
return None
|
||||
return EtcdEntry(_key, _value, value_in_json=value_in_json)
|
||||
|
||||
@readable_errors
|
||||
def put(self, *args, value_in_json=False, **kwargs):
|
||||
_key, _value = args
|
||||
if value_in_json:
|
||||
_value = json.dumps(_value)
|
||||
|
||||
if not isinstance(_key, str):
|
||||
_key = _key.decode('utf-8')
|
||||
|
||||
return self.client.put(_key, _value, **kwargs)
|
||||
|
||||
@readable_errors
|
||||
def get_prefix(self, *args, value_in_json=False, raise_exception=True, **kwargs):
|
||||
event_iterator = self.client.get_prefix(*args, **kwargs)
|
||||
for e in event_iterator:
|
||||
yield EtcdEntry(*e[::-1], value_in_json=value_in_json)
|
||||
|
||||
@readable_errors
|
||||
def watch_prefix(self, key, raise_exception=True, value_in_json=False):
|
||||
event_iterator, cancel = self.client.watch_prefix(key)
|
||||
for e in event_iterator:
|
||||
if hasattr(e, '_event'):
|
||||
e = e._event
|
||||
if e.type == e.PUT:
|
||||
yield EtcdEntry(e.kv.key, e.kv.value, value_in_json=value_in_json)
|
||||
69
uncloud_etcd_based/uncloud/common/host.py
Normal file
69
uncloud_etcd_based/uncloud/common/host.py
Normal file
|
|
@ -0,0 +1,69 @@
|
|||
import time
|
||||
from datetime import datetime
|
||||
from os.path import join
|
||||
from typing import List
|
||||
|
||||
from .classes import SpecificEtcdEntryBase
|
||||
|
||||
|
||||
class HostStatus:
|
||||
"""Possible Statuses of uncloud host."""
|
||||
|
||||
alive = "ALIVE"
|
||||
dead = "DEAD"
|
||||
|
||||
|
||||
class HostEntry(SpecificEtcdEntryBase):
|
||||
"""Represents Host Entry Structure and its supporting methods."""
|
||||
|
||||
def __init__(self, e):
|
||||
self.specs = None # type: dict
|
||||
self.hostname = None # type: str
|
||||
self.status = None # type: str
|
||||
self.last_heartbeat = None # type: str
|
||||
|
||||
super().__init__(e)
|
||||
|
||||
def update_heartbeat(self):
|
||||
self.status = HostStatus.alive
|
||||
self.last_heartbeat = datetime.utcnow().strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def is_alive(self):
|
||||
last_heartbeat = datetime.strptime(
|
||||
self.last_heartbeat, "%Y-%m-%d %H:%M:%S"
|
||||
)
|
||||
delta = datetime.utcnow() - last_heartbeat
|
||||
if delta.total_seconds() > 60:
|
||||
return False
|
||||
return True
|
||||
|
||||
def declare_dead(self):
|
||||
self.status = HostStatus.dead
|
||||
self.last_heartbeat = time.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
|
||||
class HostPool:
|
||||
def __init__(self, etcd_client, host_prefix):
|
||||
self.client = etcd_client
|
||||
self.prefix = host_prefix
|
||||
|
||||
@property
|
||||
def hosts(self) -> List[HostEntry]:
|
||||
_hosts = self.client.get_prefix(self.prefix, value_in_json=True)
|
||||
return [HostEntry(host) for host in _hosts]
|
||||
|
||||
def get(self, key):
|
||||
if not key.startswith(self.prefix):
|
||||
key = join(self.prefix, key)
|
||||
v = self.client.get(key, value_in_json=True)
|
||||
if v:
|
||||
return HostEntry(v)
|
||||
return None
|
||||
|
||||
def put(self, obj: HostEntry):
|
||||
self.client.put(obj.key, obj.value, value_in_json=True)
|
||||
|
||||
def by_status(self, status, _hosts=None):
|
||||
if _hosts is None:
|
||||
_hosts = self.hosts
|
||||
return list(filter(lambda x: x.status == status, _hosts))
|
||||
70
uncloud_etcd_based/uncloud/common/network.py
Normal file
70
uncloud_etcd_based/uncloud/common/network.py
Normal file
|
|
@ -0,0 +1,70 @@
|
|||
import subprocess as sp
|
||||
import random
|
||||
import logging
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
def random_bytes(num=6):
|
||||
return [random.randrange(256) for _ in range(num)]
|
||||
|
||||
|
||||
def generate_mac(
|
||||
uaa=False, multicast=False, oui=None, separator=":", byte_fmt="%02x"
|
||||
):
|
||||
mac = random_bytes()
|
||||
if oui:
|
||||
if type(oui) == str:
|
||||
oui = [int(chunk) for chunk in oui.split(separator)]
|
||||
mac = oui + random_bytes(num=6 - len(oui))
|
||||
else:
|
||||
if multicast:
|
||||
mac[0] |= 1 # set bit 0
|
||||
else:
|
||||
mac[0] &= ~1 # clear bit 0
|
||||
if uaa:
|
||||
mac[0] &= ~(1 << 1) # clear bit 1
|
||||
else:
|
||||
mac[0] |= 1 << 1 # set bit 1
|
||||
return separator.join(byte_fmt % b for b in mac)
|
||||
|
||||
|
||||
def create_dev(script, _id, dev, ip=None):
|
||||
command = [
|
||||
"sudo",
|
||||
"-p",
|
||||
"Enter password to create network devices for vm: ",
|
||||
script,
|
||||
str(_id),
|
||||
dev,
|
||||
]
|
||||
if ip:
|
||||
command.append(ip)
|
||||
try:
|
||||
output = sp.check_output(command, stderr=sp.PIPE)
|
||||
except Exception:
|
||||
logger.exception("Creation of interface %s failed.", dev)
|
||||
return None
|
||||
else:
|
||||
return output.decode("utf-8").strip()
|
||||
|
||||
|
||||
def delete_network_interface(iface):
|
||||
try:
|
||||
sp.check_output(
|
||||
[
|
||||
"sudo",
|
||||
"-p",
|
||||
"Enter password to remove {} network device: ".format(
|
||||
iface
|
||||
),
|
||||
"ip",
|
||||
"link",
|
||||
"del",
|
||||
iface,
|
||||
],
|
||||
stderr=sp.PIPE,
|
||||
)
|
||||
except Exception:
|
||||
logger.exception("Interface %s Deletion failed", iface)
|
||||
|
||||
13
uncloud_etcd_based/uncloud/common/parser.py
Normal file
13
uncloud_etcd_based/uncloud/common/parser.py
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
import argparse
|
||||
|
||||
|
||||
class BaseParser:
|
||||
def __init__(self, command):
|
||||
self.arg_parser = argparse.ArgumentParser(command, add_help=False)
|
||||
self.subparser = self.arg_parser.add_subparsers(dest='{}_subcommand'.format(command))
|
||||
self.common_args = {'add_help': False}
|
||||
|
||||
methods = [attr for attr in dir(self) if not attr.startswith('__')
|
||||
and type(getattr(self, attr)).__name__ == 'method']
|
||||
for method in methods:
|
||||
getattr(self, method)(**self.common_args)
|
||||
46
uncloud_etcd_based/uncloud/common/request.py
Normal file
46
uncloud_etcd_based/uncloud/common/request.py
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import json
|
||||
from os.path import join
|
||||
from uuid import uuid4
|
||||
|
||||
from uncloud.common.etcd_wrapper import EtcdEntry
|
||||
from uncloud.common.classes import SpecificEtcdEntryBase
|
||||
|
||||
|
||||
class RequestType:
|
||||
CreateVM = "CreateVM"
|
||||
ScheduleVM = "ScheduleVM"
|
||||
StartVM = "StartVM"
|
||||
StopVM = "StopVM"
|
||||
InitVMMigration = "InitVMMigration"
|
||||
TransferVM = "TransferVM"
|
||||
DeleteVM = "DeleteVM"
|
||||
|
||||
|
||||
class RequestEntry(SpecificEtcdEntryBase):
|
||||
def __init__(self, e):
|
||||
self.destination_sock_path = None
|
||||
self.destination_host_key = None
|
||||
self.type = None # type: str
|
||||
self.migration = None # type: bool
|
||||
self.destination = None # type: str
|
||||
self.uuid = None # type: str
|
||||
self.hostname = None # type: str
|
||||
super().__init__(e)
|
||||
|
||||
@classmethod
|
||||
def from_scratch(cls, request_prefix, **kwargs):
|
||||
e = EtcdEntry(meta_or_key=join(request_prefix, uuid4().hex),
|
||||
value=json.dumps(kwargs).encode('utf-8'), value_in_json=True)
|
||||
return cls(e)
|
||||
|
||||
|
||||
class RequestPool:
|
||||
def __init__(self, etcd_client, request_prefix):
|
||||
self.client = etcd_client
|
||||
self.prefix = request_prefix
|
||||
|
||||
def put(self, obj: RequestEntry):
|
||||
if not obj.key.startswith(self.prefix):
|
||||
obj.key = join(self.prefix, obj.key)
|
||||
|
||||
self.client.put(obj.key, obj.value, value_in_json=True)
|
||||
41
uncloud_etcd_based/uncloud/common/schemas.py
Normal file
41
uncloud_etcd_based/uncloud/common/schemas.py
Normal file
|
|
@ -0,0 +1,41 @@
|
|||
import bitmath
|
||||
|
||||
from marshmallow import fields, Schema
|
||||
|
||||
|
||||
class StorageUnit(fields.Field):
|
||||
def _serialize(self, value, attr, obj, **kwargs):
|
||||
return str(value)
|
||||
|
||||
def _deserialize(self, value, attr, data, **kwargs):
|
||||
return bitmath.parse_string_unsafe(value)
|
||||
|
||||
|
||||
class SpecsSchema(Schema):
|
||||
cpu = fields.Int()
|
||||
ram = StorageUnit()
|
||||
os_ssd = StorageUnit(data_key="os-ssd", attribute="os-ssd")
|
||||
hdd = fields.List(StorageUnit())
|
||||
|
||||
|
||||
class VMSchema(Schema):
|
||||
name = fields.Str()
|
||||
owner = fields.Str()
|
||||
owner_realm = fields.Str()
|
||||
specs = fields.Nested(SpecsSchema)
|
||||
status = fields.Str()
|
||||
log = fields.List(fields.Str())
|
||||
vnc_socket = fields.Str()
|
||||
image_uuid = fields.Str()
|
||||
hostname = fields.Str()
|
||||
metadata = fields.Dict()
|
||||
network = fields.List(
|
||||
fields.Tuple((fields.Str(), fields.Str(), fields.Int()))
|
||||
)
|
||||
in_migration = fields.Bool()
|
||||
|
||||
|
||||
class NetworkSchema(Schema):
|
||||
_id = fields.Int(data_key="id", attribute="id")
|
||||
_type = fields.Str(data_key="type", attribute="type")
|
||||
ipv6 = fields.Str()
|
||||
136
uncloud_etcd_based/uncloud/common/settings.py
Normal file
136
uncloud_etcd_based/uncloud/common/settings.py
Normal file
|
|
@ -0,0 +1,136 @@
|
|||
import configparser
|
||||
import logging
|
||||
import sys
|
||||
import os
|
||||
|
||||
from datetime import datetime
|
||||
from uncloud.common.etcd_wrapper import Etcd3Wrapper
|
||||
from os.path import join as join_path
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
settings = None
|
||||
|
||||
|
||||
class CustomConfigParser(configparser.RawConfigParser):
|
||||
def __getitem__(self, key):
|
||||
try:
|
||||
result = super().__getitem__(key)
|
||||
except KeyError as err:
|
||||
raise KeyError(
|
||||
'Key \'{}\' not found in configuration. Make sure you configure uncloud.'.format(
|
||||
key
|
||||
)
|
||||
) from err
|
||||
else:
|
||||
return result
|
||||
|
||||
|
||||
class Settings(object):
|
||||
def __init__(self, conf_dir, seed_value=None):
|
||||
conf_name = 'uncloud.conf'
|
||||
self.config_file = join_path(conf_dir, conf_name)
|
||||
|
||||
# this is used to cache config from etcd for 1 minutes. Without this we
|
||||
# would make a lot of requests to etcd which slows down everything.
|
||||
self.last_config_update = datetime.fromtimestamp(0)
|
||||
|
||||
self.config_parser = CustomConfigParser(allow_no_value=True)
|
||||
self.config_parser.add_section('etcd')
|
||||
self.config_parser.set('etcd', 'base_prefix', '/')
|
||||
|
||||
if os.access(self.config_file, os.R_OK):
|
||||
self.config_parser.read(self.config_file)
|
||||
else:
|
||||
raise FileNotFoundError('Config file %s not found!', self.config_file)
|
||||
self.config_key = join_path(self['etcd']['base_prefix'] + 'uncloud/config/')
|
||||
|
||||
self.read_internal_values()
|
||||
|
||||
if seed_value is None:
|
||||
seed_value = dict()
|
||||
|
||||
self.config_parser.read_dict(seed_value)
|
||||
|
||||
def get_etcd_client(self):
|
||||
args = tuple()
|
||||
try:
|
||||
kwargs = {
|
||||
'host': self.config_parser.get('etcd', 'url'),
|
||||
'port': self.config_parser.get('etcd', 'port'),
|
||||
'ca_cert': self.config_parser.get('etcd', 'ca_cert'),
|
||||
'cert_cert': self.config_parser.get('etcd', 'cert_cert'),
|
||||
'cert_key': self.config_parser.get('etcd', 'cert_key'),
|
||||
}
|
||||
except configparser.Error as err:
|
||||
raise configparser.Error(
|
||||
'{} in config file {}'.format(
|
||||
err.message, self.config_file
|
||||
)
|
||||
) from err
|
||||
else:
|
||||
try:
|
||||
wrapper = Etcd3Wrapper(*args, **kwargs)
|
||||
except Exception as err:
|
||||
logger.error(
|
||||
'etcd connection not successfull. Please check your config file.'
|
||||
'\nDetails: %s\netcd connection parameters: %s',
|
||||
err,
|
||||
kwargs,
|
||||
)
|
||||
sys.exit(1)
|
||||
else:
|
||||
return wrapper
|
||||
|
||||
def read_internal_values(self):
|
||||
base_prefix = self['etcd']['base_prefix']
|
||||
self.config_parser.read_dict(
|
||||
{
|
||||
'etcd': {
|
||||
'file_prefix': join_path(base_prefix, 'files/'),
|
||||
'host_prefix': join_path(base_prefix, 'hosts/'),
|
||||
'image_prefix': join_path(base_prefix, 'images/'),
|
||||
'image_store_prefix': join_path(base_prefix, 'imagestore/'),
|
||||
'network_prefix': join_path(base_prefix, 'networks/'),
|
||||
'request_prefix': join_path(base_prefix, 'requests/'),
|
||||
'user_prefix': join_path(base_prefix, 'users/'),
|
||||
'vm_prefix': join_path(base_prefix, 'vms/'),
|
||||
'vxlan_counter': join_path(base_prefix, 'counters/vxlan'),
|
||||
'tap_counter': join_path(base_prefix, 'counters/tap')
|
||||
}
|
||||
}
|
||||
)
|
||||
|
||||
def read_config_file_values(self, config_file):
|
||||
try:
|
||||
# Trying to read configuration file
|
||||
with open(config_file) as config_file_handle:
|
||||
self.config_parser.read_file(config_file_handle)
|
||||
except FileNotFoundError:
|
||||
sys.exit('Configuration file {} not found!'.format(config_file))
|
||||
except Exception as err:
|
||||
logger.exception(err)
|
||||
sys.exit('Error occurred while reading configuration file')
|
||||
|
||||
def read_values_from_etcd(self):
|
||||
etcd_client = self.get_etcd_client()
|
||||
if (datetime.utcnow() - self.last_config_update).total_seconds() > 60:
|
||||
config_from_etcd = etcd_client.get(self.config_key, value_in_json=True)
|
||||
if config_from_etcd:
|
||||
self.config_parser.read_dict(config_from_etcd.value)
|
||||
self.last_config_update = datetime.utcnow()
|
||||
else:
|
||||
raise KeyError('Key \'{}\' not found in etcd. Please configure uncloud.'.format(self.config_key))
|
||||
|
||||
def __getitem__(self, key):
|
||||
# Allow failing to read from etcd if we have
|
||||
# it locally
|
||||
if key not in self.config_parser.sections():
|
||||
try:
|
||||
self.read_values_from_etcd()
|
||||
except KeyError:
|
||||
pass
|
||||
return self.config_parser[key]
|
||||
|
||||
|
||||
def get_settings():
|
||||
return settings
|
||||
34
uncloud_etcd_based/uncloud/common/shared.py
Normal file
34
uncloud_etcd_based/uncloud/common/shared.py
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
from uncloud.common.settings import get_settings
|
||||
from uncloud.common.vm import VmPool
|
||||
from uncloud.common.host import HostPool
|
||||
from uncloud.common.request import RequestPool
|
||||
import uncloud.common.storage_handlers as storage_handlers
|
||||
|
||||
|
||||
class Shared:
|
||||
@property
|
||||
def settings(self):
|
||||
return get_settings()
|
||||
|
||||
@property
|
||||
def etcd_client(self):
|
||||
return self.settings.get_etcd_client()
|
||||
|
||||
@property
|
||||
def host_pool(self):
|
||||
return HostPool(self.etcd_client, self.settings["etcd"]["host_prefix"])
|
||||
|
||||
@property
|
||||
def vm_pool(self):
|
||||
return VmPool(self.etcd_client, self.settings["etcd"]["vm_prefix"])
|
||||
|
||||
@property
|
||||
def request_pool(self):
|
||||
return RequestPool(self.etcd_client, self.settings["etcd"]["request_prefix"])
|
||||
|
||||
@property
|
||||
def storage_handler(self):
|
||||
return storage_handlers.get_storage_handler()
|
||||
|
||||
|
||||
shared = Shared()
|
||||
207
uncloud_etcd_based/uncloud/common/storage_handlers.py
Normal file
207
uncloud_etcd_based/uncloud/common/storage_handlers.py
Normal file
|
|
@ -0,0 +1,207 @@
|
|||
import shutil
|
||||
import subprocess as sp
|
||||
import os
|
||||
import stat
|
||||
|
||||
from abc import ABC
|
||||
from . import logger
|
||||
from os.path import join as join_path
|
||||
import uncloud.common.shared as shared
|
||||
|
||||
|
||||
class ImageStorageHandler(ABC):
|
||||
handler_name = "base"
|
||||
|
||||
def __init__(self, image_base, vm_base):
|
||||
self.image_base = image_base
|
||||
self.vm_base = vm_base
|
||||
|
||||
def import_image(self, image_src, image_dest, protect=False):
|
||||
"""Put an image at the destination
|
||||
:param image_src: An Image file
|
||||
:param image_dest: A path where :param src: is to be put.
|
||||
:param protect: If protect is true then the dest is protect (readonly etc)
|
||||
The obj must exist on filesystem.
|
||||
"""
|
||||
|
||||
raise NotImplementedError()
|
||||
|
||||
def make_vm_image(self, image_path, path):
|
||||
"""Copy image from src to dest
|
||||
|
||||
:param image_path: A path
|
||||
:param path: A path
|
||||
|
||||
src and destination must be on same storage system i.e both on file system or both on CEPH etc.
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def resize_vm_image(self, path, size):
|
||||
"""Resize image located at :param path:
|
||||
:param path: The file which is to be resized
|
||||
:param size: Size must be in Megabytes
|
||||
"""
|
||||
raise NotImplementedError()
|
||||
|
||||
def delete_vm_image(self, path):
|
||||
raise NotImplementedError()
|
||||
|
||||
def execute_command(self, command, report=True, error_origin=None):
|
||||
if not error_origin:
|
||||
error_origin = self.handler_name
|
||||
|
||||
command = list(map(str, command))
|
||||
try:
|
||||
sp.check_output(command, stderr=sp.PIPE)
|
||||
except sp.CalledProcessError as e:
|
||||
_stderr = e.stderr.decode("utf-8").strip()
|
||||
if report:
|
||||
logger.exception("%s:- %s", error_origin, _stderr)
|
||||
return False
|
||||
return True
|
||||
|
||||
def vm_path_string(self, path):
|
||||
raise NotImplementedError()
|
||||
|
||||
def qemu_path_string(self, path):
|
||||
raise NotImplementedError()
|
||||
|
||||
def is_vm_image_exists(self, path):
|
||||
raise NotImplementedError()
|
||||
|
||||
|
||||
class FileSystemBasedImageStorageHandler(ImageStorageHandler):
|
||||
handler_name = "Filesystem"
|
||||
|
||||
def import_image(self, src, dest, protect=False):
|
||||
dest = join_path(self.image_base, dest)
|
||||
try:
|
||||
shutil.copy(src, dest)
|
||||
if protect:
|
||||
os.chmod(
|
||||
dest, stat.S_IRUSR | stat.S_IRGRP | stat.S_IROTH
|
||||
)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
def make_vm_image(self, src, dest):
|
||||
src = join_path(self.image_base, src)
|
||||
dest = join_path(self.vm_base, dest)
|
||||
try:
|
||||
shutil.copyfile(src, dest)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
def resize_vm_image(self, path, size):
|
||||
path = join_path(self.vm_base, path)
|
||||
command = [
|
||||
"qemu-img",
|
||||
"resize",
|
||||
"-f",
|
||||
"raw",
|
||||
path,
|
||||
"{}M".format(size),
|
||||
]
|
||||
if self.execute_command(command):
|
||||
return True
|
||||
else:
|
||||
self.delete_vm_image(path)
|
||||
return False
|
||||
|
||||
def delete_vm_image(self, path):
|
||||
path = join_path(self.vm_base, path)
|
||||
try:
|
||||
os.remove(path)
|
||||
except Exception as e:
|
||||
logger.exception(e)
|
||||
return False
|
||||
return True
|
||||
|
||||
def vm_path_string(self, path):
|
||||
return join_path(self.vm_base, path)
|
||||
|
||||
def qemu_path_string(self, path):
|
||||
return self.vm_path_string(path)
|
||||
|
||||
def is_vm_image_exists(self, path):
|
||||
path = join_path(self.vm_base, path)
|
||||
command = ["ls", path]
|
||||
return self.execute_command(command, report=False)
|
||||
|
||||
|
||||
class CEPHBasedImageStorageHandler(ImageStorageHandler):
|
||||
handler_name = "Ceph"
|
||||
|
||||
def import_image(self, src, dest, protect=False):
|
||||
dest = join_path(self.image_base, dest)
|
||||
import_command = ["rbd", "import", src, dest]
|
||||
commands = [import_command]
|
||||
if protect:
|
||||
snap_create_command = [
|
||||
"rbd",
|
||||
"snap",
|
||||
"create",
|
||||
"{}@protected".format(dest),
|
||||
]
|
||||
snap_protect_command = [
|
||||
"rbd",
|
||||
"snap",
|
||||
"protect",
|
||||
"{}@protected".format(dest),
|
||||
]
|
||||
commands.append(snap_create_command)
|
||||
commands.append(snap_protect_command)
|
||||
|
||||
result = True
|
||||
for command in commands:
|
||||
result = result and self.execute_command(command)
|
||||
|
||||
return result
|
||||
|
||||
def make_vm_image(self, src, dest):
|
||||
src = join_path(self.image_base, src)
|
||||
dest = join_path(self.vm_base, dest)
|
||||
|
||||
command = ["rbd", "clone", "{}@protected".format(src), dest]
|
||||
return self.execute_command(command)
|
||||
|
||||
def resize_vm_image(self, path, size):
|
||||
path = join_path(self.vm_base, path)
|
||||
command = ["rbd", "resize", path, "--size", size]
|
||||
return self.execute_command(command)
|
||||
|
||||
def delete_vm_image(self, path):
|
||||
path = join_path(self.vm_base, path)
|
||||
command = ["rbd", "rm", path]
|
||||
return self.execute_command(command)
|
||||
|
||||
def vm_path_string(self, path):
|
||||
return join_path(self.vm_base, path)
|
||||
|
||||
def qemu_path_string(self, path):
|
||||
return "rbd:{}".format(self.vm_path_string(path))
|
||||
|
||||
def is_vm_image_exists(self, path):
|
||||
path = join_path(self.vm_base, path)
|
||||
command = ["rbd", "info", path]
|
||||
return self.execute_command(command, report=False)
|
||||
|
||||
|
||||
def get_storage_handler():
|
||||
__storage_backend = shared.shared.settings["storage"]["storage_backend"]
|
||||
if __storage_backend == "filesystem":
|
||||
return FileSystemBasedImageStorageHandler(
|
||||
vm_base=shared.shared.settings["storage"]["vm_dir"],
|
||||
image_base=shared.shared.settings["storage"]["image_dir"],
|
||||
)
|
||||
elif __storage_backend == "ceph":
|
||||
return CEPHBasedImageStorageHandler(
|
||||
vm_base=shared.shared.settings["storage"]["ceph_vm_pool"],
|
||||
image_base=shared.shared.settings["storage"]["ceph_image_pool"],
|
||||
)
|
||||
else:
|
||||
raise Exception("Unknown Image Storage Handler")
|
||||
102
uncloud_etcd_based/uncloud/common/vm.py
Normal file
102
uncloud_etcd_based/uncloud/common/vm.py
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
from contextlib import contextmanager
|
||||
from datetime import datetime
|
||||
from os.path import join
|
||||
|
||||
from .classes import SpecificEtcdEntryBase
|
||||
|
||||
|
||||
class VMStatus:
|
||||
stopped = "STOPPED" # After requested_shutdown
|
||||
killed = "KILLED" # either host died or vm died itself
|
||||
running = "RUNNING"
|
||||
error = "ERROR" # An error occurred that cannot be resolved automatically
|
||||
|
||||
|
||||
def declare_stopped(vm):
|
||||
vm["hostname"] = ""
|
||||
vm["in_migration"] = False
|
||||
vm["status"] = VMStatus.stopped
|
||||
|
||||
|
||||
class VMEntry(SpecificEtcdEntryBase):
|
||||
def __init__(self, e):
|
||||
self.owner = None # type: str
|
||||
self.specs = None # type: dict
|
||||
self.hostname = None # type: str
|
||||
self.status = None # type: str
|
||||
self.image_uuid = None # type: str
|
||||
self.log = None # type: list
|
||||
self.in_migration = None # type: bool
|
||||
|
||||
super().__init__(e)
|
||||
|
||||
@property
|
||||
def uuid(self):
|
||||
return self.key.split("/")[-1]
|
||||
|
||||
def declare_killed(self):
|
||||
self.hostname = ""
|
||||
self.in_migration = False
|
||||
if self.status == VMStatus.running:
|
||||
self.status = VMStatus.killed
|
||||
|
||||
def declare_stopped(self):
|
||||
self.hostname = ""
|
||||
self.in_migration = False
|
||||
self.status = VMStatus.stopped
|
||||
|
||||
def add_log(self, msg):
|
||||
self.log = self.log[:5]
|
||||
self.log.append(
|
||||
"{} - {}".format(datetime.now().isoformat(), msg)
|
||||
)
|
||||
|
||||
|
||||
class VmPool:
|
||||
def __init__(self, etcd_client, vm_prefix):
|
||||
self.client = etcd_client
|
||||
self.prefix = vm_prefix
|
||||
|
||||
@property
|
||||
def vms(self):
|
||||
_vms = self.client.get_prefix(self.prefix, value_in_json=True)
|
||||
return [VMEntry(vm) for vm in _vms]
|
||||
|
||||
def by_host(self, host, _vms=None):
|
||||
if _vms is None:
|
||||
_vms = self.vms
|
||||
return list(filter(lambda x: x.hostname == host, _vms))
|
||||
|
||||
def by_status(self, status, _vms=None):
|
||||
if _vms is None:
|
||||
_vms = self.vms
|
||||
return list(filter(lambda x: x.status == status, _vms))
|
||||
|
||||
def by_owner(self, owner, _vms=None):
|
||||
if _vms is None:
|
||||
_vms = self.vms
|
||||
return list(filter(lambda x: x.owner == owner, _vms))
|
||||
|
||||
def except_status(self, status, _vms=None):
|
||||
if _vms is None:
|
||||
_vms = self.vms
|
||||
return list(filter(lambda x: x.status != status, _vms))
|
||||
|
||||
def get(self, key):
|
||||
if not key.startswith(self.prefix):
|
||||
key = join(self.prefix, key)
|
||||
v = self.client.get(key, value_in_json=True)
|
||||
if v:
|
||||
return VMEntry(v)
|
||||
return None
|
||||
|
||||
def put(self, obj: VMEntry):
|
||||
self.client.put(obj.key, obj.value, value_in_json=True)
|
||||
|
||||
@contextmanager
|
||||
def get_put(self, key) -> VMEntry:
|
||||
# Updates object at key on exit
|
||||
obj = self.get(key)
|
||||
yield obj
|
||||
if obj:
|
||||
self.put(obj)
|
||||
Loading…
Add table
Add a link
Reference in a new issue