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
13
uncloud_etcd_based/uncloud/hack/README.org
Normal file
13
uncloud_etcd_based/uncloud/hack/README.org
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
This directory contains unfinishe hacks / inspirations
|
||||
* firewalling / networking in ucloud
|
||||
** automatically route a network per VM - /64?
|
||||
** nft: one chain per VM on each vm host (?)
|
||||
*** might have scaling issues?
|
||||
** firewall rules on each VM host
|
||||
- mac filtering:
|
||||
* To add / block
|
||||
** TODO arp poisoning
|
||||
** TODO ndp "poisoning"
|
||||
** TODO ipv4 dhcp server
|
||||
*** drop dhcpv4 requests
|
||||
*** drop dhcpv4 answers
|
||||
1
uncloud_etcd_based/uncloud/hack/__init__.py
Normal file
1
uncloud_etcd_based/uncloud/hack/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
1
uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host
Normal file
1
uncloud_etcd_based/uncloud/hack/conf.d/ucloud-host
Normal file
|
|
@ -0,0 +1 @@
|
|||
HOSTNAME=server1.place10
|
||||
39
uncloud_etcd_based/uncloud/hack/config.py
Normal file
39
uncloud_etcd_based/uncloud/hack/config.py
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
|
||||
#
|
||||
# This file is part of uncloud.
|
||||
#
|
||||
# uncloud is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# uncloud is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
class Config(object):
|
||||
def __init__(self, arguments):
|
||||
""" read arguments dicts as a base """
|
||||
|
||||
self.arguments = arguments
|
||||
|
||||
# Split them so *etcd_args can be used and we can
|
||||
# iterate over etcd_hosts
|
||||
self.etcd_hosts = [ arguments['etcd_host'] ]
|
||||
self.etcd_args = {
|
||||
'ca_cert': arguments['etcd_ca_cert'],
|
||||
'cert_cert': arguments['etcd_cert_cert'],
|
||||
'cert_key': arguments['etcd_cert_key'],
|
||||
# 'user': None,
|
||||
# 'password': None
|
||||
}
|
||||
self.etcd_prefix = '/nicohack/'
|
||||
149
uncloud_etcd_based/uncloud/hack/db.py
Normal file
149
uncloud_etcd_based/uncloud/hack/db.py
Normal file
|
|
@ -0,0 +1,149 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
|
||||
#
|
||||
# This file is part of uncloud.
|
||||
#
|
||||
# uncloud is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# uncloud is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import etcd3
|
||||
import json
|
||||
import logging
|
||||
import datetime
|
||||
import re
|
||||
|
||||
from functools import wraps
|
||||
from uncloud import UncloudException
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def db_logentry(message):
|
||||
timestamp = datetime.datetime.now()
|
||||
return {
|
||||
"timestamp": str(timestamp),
|
||||
"message": message
|
||||
}
|
||||
|
||||
|
||||
def readable_errors(func):
|
||||
@wraps(func)
|
||||
def wrapper(*args, **kwargs):
|
||||
try:
|
||||
return func(*args, **kwargs)
|
||||
except etcd3.exceptions.ConnectionFailedError as e:
|
||||
raise UncloudException('Cannot connect to etcd: is etcd running and reachable? {}'.format(e))
|
||||
except etcd3.exceptions.ConnectionTimeoutError as e:
|
||||
raise UncloudException('etcd connection timeout. {}'.format(e))
|
||||
|
||||
return wrapper
|
||||
|
||||
|
||||
class DB(object):
|
||||
def __init__(self, config, prefix="/"):
|
||||
self.config = config
|
||||
|
||||
# Root for everything
|
||||
self.base_prefix= '/nicohack'
|
||||
|
||||
# Can be set from outside
|
||||
self.prefix = prefix
|
||||
|
||||
try:
|
||||
self.connect()
|
||||
except FileNotFoundError as e:
|
||||
raise UncloudException("Is the path to the etcd certs correct? {}".format(e))
|
||||
|
||||
@readable_errors
|
||||
def connect(self):
|
||||
self._db_clients = []
|
||||
for endpoint in self.config.etcd_hosts:
|
||||
client = etcd3.client(host=endpoint, **self.config.etcd_args)
|
||||
self._db_clients.append(client)
|
||||
|
||||
def realkey(self, key):
|
||||
return "{}{}/{}".format(self.base_prefix,
|
||||
self.prefix,
|
||||
key)
|
||||
|
||||
@readable_errors
|
||||
def get(self, key, as_json=False, **kwargs):
|
||||
value, _ = self._db_clients[0].get(self.realkey(key), **kwargs)
|
||||
|
||||
if as_json:
|
||||
value = json.loads(value)
|
||||
|
||||
return value
|
||||
|
||||
@readable_errors
|
||||
def get_prefix(self, key, as_json=False, **kwargs):
|
||||
for value, meta in self._db_clients[0].get_prefix(self.realkey(key), **kwargs):
|
||||
k = meta.key.decode("utf-8")
|
||||
value = value.decode("utf-8")
|
||||
if as_json:
|
||||
value = json.loads(value)
|
||||
|
||||
yield (k, value)
|
||||
|
||||
|
||||
@readable_errors
|
||||
def set(self, key, value, as_json=False, **kwargs):
|
||||
if as_json:
|
||||
value = json.dumps(value)
|
||||
|
||||
log.debug("Setting {} = {}".format(self.realkey(key), value))
|
||||
# FIXME: iterate over clients in case of failure ?
|
||||
return self._db_clients[0].put(self.realkey(key), value, **kwargs)
|
||||
|
||||
|
||||
@readable_errors
|
||||
def list_and_filter(self, key, filter_key=None, filter_regexp=None):
|
||||
for k,v in self.get_prefix(key, as_json=True):
|
||||
|
||||
if filter_key and filter_regexp:
|
||||
if filter_key in v:
|
||||
if re.match(filter_regexp, v[filter_key]):
|
||||
yield v
|
||||
else:
|
||||
yield v
|
||||
|
||||
|
||||
@readable_errors
|
||||
def increment(self, key, **kwargs):
|
||||
print(self.realkey(key))
|
||||
|
||||
|
||||
print("prelock")
|
||||
lock = self._db_clients[0].lock('/nicohack/foo')
|
||||
print("prelockacq")
|
||||
lock.acquire()
|
||||
print("prelockrelease")
|
||||
lock.release()
|
||||
|
||||
with self._db_clients[0].lock("/nicohack/mac/last_used_index") as lock:
|
||||
print("in lock")
|
||||
pass
|
||||
|
||||
# with self._db_clients[0].lock(self.realkey(key)) as lock:# value = int(self.get(self.realkey(key), **kwargs))
|
||||
# self.set(self.realkey(key), str(value + 1), **kwargs)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
endpoints = [ "https://etcd1.ungleich.ch:2379",
|
||||
"https://etcd2.ungleich.ch:2379",
|
||||
"https://etcd3.ungleich.ch:2379" ]
|
||||
|
||||
db = DB(url=endpoints)
|
||||
3
uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore
vendored
Normal file
3
uncloud_etcd_based/uncloud/hack/hackcloud/.gitignore
vendored
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
*.iso
|
||||
radvdpid
|
||||
foo
|
||||
1
uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py
Normal file
1
uncloud_etcd_based/uncloud/hack/hackcloud/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
|
||||
6
uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh
Normal file
6
uncloud_etcd_based/uncloud/hack/hackcloud/etcd-client.sh
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#!/bin/sh
|
||||
|
||||
etcdctl --cert=$HOME/vcs/ungleich-dot-cdist/files/etcd/nico.pem \
|
||||
--key=/home/nico/vcs/ungleich-dot-cdist/files/etcd/nico-key.pem \
|
||||
--cacert=$HOME/vcs/ungleich-dot-cdist/files/etcd/ca.pem \
|
||||
--endpoints https://etcd1.ungleich.ch:2379,https://etcd2.ungleich.ch:2379,https://etcd3.ungleich.ch:2379 "$@"
|
||||
3
uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh
Executable file
3
uncloud_etcd_based/uncloud/hack/hackcloud/ifdown.sh
Executable file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
echo $@
|
||||
7
uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh
Executable file
7
uncloud_etcd_based/uncloud/hack/hackcloud/ifup.sh
Executable file
|
|
@ -0,0 +1,7 @@
|
|||
#!/bin/sh
|
||||
|
||||
dev=$1; shift
|
||||
|
||||
# bridge is setup from outside
|
||||
ip link set dev "$dev" master ${bridge}
|
||||
ip link set dev "$dev" up
|
||||
1
uncloud_etcd_based/uncloud/hack/hackcloud/mac-last
Normal file
1
uncloud_etcd_based/uncloud/hack/hackcloud/mac-last
Normal file
|
|
@ -0,0 +1 @@
|
|||
000000000252
|
||||
1
uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix
Normal file
1
uncloud_etcd_based/uncloud/hack/hackcloud/mac-prefix
Normal file
|
|
@ -0,0 +1 @@
|
|||
02:00
|
||||
29
uncloud_etcd_based/uncloud/hack/hackcloud/net.sh
Executable file
29
uncloud_etcd_based/uncloud/hack/hackcloud/net.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/sh
|
||||
|
||||
set -x
|
||||
|
||||
netid=100
|
||||
dev=wlp2s0
|
||||
dev=wlp0s20f3
|
||||
#dev=wlan0
|
||||
|
||||
ip=2a0a:e5c1:111:888::48/64
|
||||
vxlandev=vxlan${netid}
|
||||
bridgedev=br${netid}
|
||||
|
||||
ip -6 link add ${vxlandev} type vxlan \
|
||||
id ${netid} \
|
||||
dstport 4789 \
|
||||
group ff05::${netid} \
|
||||
dev ${dev} \
|
||||
ttl 5
|
||||
|
||||
ip link set ${vxlandev} up
|
||||
|
||||
|
||||
ip link add ${bridgedev} type bridge
|
||||
ip link set ${bridgedev} up
|
||||
|
||||
ip link set ${vxlandev} master ${bridgedev} up
|
||||
|
||||
ip addr add ${ip} dev ${bridgedev}
|
||||
31
uncloud_etcd_based/uncloud/hack/hackcloud/nftrules
Normal file
31
uncloud_etcd_based/uncloud/hack/hackcloud/nftrules
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
flush ruleset
|
||||
|
||||
table bridge filter {
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority 0;
|
||||
policy accept;
|
||||
|
||||
ibrname br100 jump br100
|
||||
}
|
||||
|
||||
chain br100 {
|
||||
# Allow all incoming traffic from outside
|
||||
iifname vxlan100 accept
|
||||
|
||||
# Default blocks: router advertisements, dhcpv6, dhcpv4
|
||||
icmpv6 type nd-router-advert drop
|
||||
ip6 version 6 udp sport 547 drop
|
||||
ip version 4 udp sport 67 drop
|
||||
|
||||
jump br100_vmlist
|
||||
drop
|
||||
}
|
||||
chain br100_vmlist {
|
||||
# VM1
|
||||
iifname tap1 ether saddr 02:00:f0:a9:c4:4e ip6 saddr 2a0a:e5c1:111:888:0:f0ff:fea9:c44e accept
|
||||
|
||||
# VM2
|
||||
iifname v343a-0 ether saddr 02:00:f0:a9:c4:4f ip6 saddr 2a0a:e5c1:111:888:0:f0ff:fea9:c44f accept
|
||||
iifname v343a-0 ether saddr 02:00:f0:a9:c4:4f ip6 saddr 2a0a:e5c1:111:1234::/64 accept
|
||||
}
|
||||
}
|
||||
13
uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf
Normal file
13
uncloud_etcd_based/uncloud/hack/hackcloud/radvd.conf
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
interface br100
|
||||
{
|
||||
AdvSendAdvert on;
|
||||
MinRtrAdvInterval 3;
|
||||
MaxRtrAdvInterval 5;
|
||||
AdvDefaultLifetime 3600;
|
||||
|
||||
prefix 2a0a:e5c1:111:888::/64 {
|
||||
};
|
||||
|
||||
RDNSS 2a0a:e5c0::3 2a0a:e5c0::4 { AdvRDNSSLifetime 6000; };
|
||||
DNSSL place7.ungleich.ch { AdvDNSSLLifetime 6000; } ;
|
||||
};
|
||||
3
uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh
Normal file
3
uncloud_etcd_based/uncloud/hack/hackcloud/radvd.sh
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
#!/bin/sh
|
||||
|
||||
radvd -C ./radvd.conf -n -p ./radvdpid
|
||||
29
uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh
Executable file
29
uncloud_etcd_based/uncloud/hack/hackcloud/vm.sh
Executable file
|
|
@ -0,0 +1,29 @@
|
|||
#!/bin/sh
|
||||
|
||||
# if [ $# -ne 1 ]; then
|
||||
# echo "$0: owner"
|
||||
# exit 1
|
||||
# fi
|
||||
|
||||
qemu=/usr/bin/qemu-system-x86_64
|
||||
|
||||
accel=kvm
|
||||
#accel=tcg
|
||||
|
||||
memory=1024
|
||||
cores=2
|
||||
uuid=$(uuidgen)
|
||||
mac=$(./mac-gen.py)
|
||||
owner=nico
|
||||
|
||||
export bridge=br100
|
||||
|
||||
set -x
|
||||
$qemu -name "uncloud-${uuid}" \
|
||||
-machine pc,accel=${accel} \
|
||||
-m ${memory} \
|
||||
-smp ${cores} \
|
||||
-uuid ${uuid} \
|
||||
-drive file=alpine-virt-3.11.2-x86_64.iso,media=cdrom \
|
||||
-netdev tap,id=netmain,script=./ifup.sh,downscript=./ifdown.sh \
|
||||
-device virtio-net-pci,netdev=netmain,id=net0,mac=${mac}
|
||||
75
uncloud_etcd_based/uncloud/hack/host.py
Normal file
75
uncloud_etcd_based/uncloud/hack/host.py
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
|
||||
#
|
||||
# This file is part of uncloud.
|
||||
#
|
||||
# uncloud is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# uncloud is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import uuid
|
||||
|
||||
from uncloud.hack.db import DB
|
||||
from uncloud import UncloudException
|
||||
|
||||
class Host(object):
|
||||
def __init__(self, config, db_entry=None):
|
||||
self.config = config
|
||||
self.db = DB(self.config, prefix="/hosts")
|
||||
|
||||
if db_entry:
|
||||
self.db_entry = db_entry
|
||||
|
||||
|
||||
def list_hosts(self, filter_key=None, filter_regexp=None):
|
||||
""" Return list of all hosts """
|
||||
for entry in self.db.list_and_filter("", filter_key, filter_regexp):
|
||||
yield self.__class__(self.config, db_entry=entry)
|
||||
|
||||
def cmdline_add_host(self):
|
||||
""" FIXME: make this a bit smarter and less redundant """
|
||||
|
||||
for required_arg in [
|
||||
'add_vm_host',
|
||||
'max_cores_per_vm',
|
||||
'max_cores_total',
|
||||
'max_memory_in_gb' ]:
|
||||
if not required_arg in self.config.arguments:
|
||||
raise UncloudException("Missing argument: {}".format(required_arg))
|
||||
|
||||
return self.add_host(
|
||||
self.config.arguments['add_vm_host'],
|
||||
self.config.arguments['max_cores_per_vm'],
|
||||
self.config.arguments['max_cores_total'],
|
||||
self.config.arguments['max_memory_in_gb'])
|
||||
|
||||
|
||||
def add_host(self,
|
||||
hostname,
|
||||
max_cores_per_vm,
|
||||
max_cores_total,
|
||||
max_memory_in_gb):
|
||||
|
||||
db_entry = {}
|
||||
db_entry['uuid'] = str(uuid.uuid4())
|
||||
db_entry['hostname'] = hostname
|
||||
db_entry['max_cores_per_vm'] = max_cores_per_vm
|
||||
db_entry['max_cores_total'] = max_cores_total
|
||||
db_entry['max_memory_in_gb'] = max_memory_in_gb
|
||||
db_entry["db_version"] = 1
|
||||
db_entry["log"] = []
|
||||
|
||||
self.db.set(db_entry['uuid'], db_entry, as_json=True)
|
||||
|
||||
return self.__class__(self.config, db_entry)
|
||||
104
uncloud_etcd_based/uncloud/hack/mac.py
Executable file
104
uncloud_etcd_based/uncloud/hack/mac.py
Executable file
|
|
@ -0,0 +1,104 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2012 Nico Schottelius (nico-cinv at schottelius.org)
|
||||
#
|
||||
# This file is part of cinv.
|
||||
#
|
||||
# cinv is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# cinv is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with cinv. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
#
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import os.path
|
||||
import os
|
||||
import re
|
||||
import json
|
||||
|
||||
from uncloud import UncloudException
|
||||
from uncloud.hack.db import DB
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class MAC(object):
|
||||
def __init__(self, config):
|
||||
self.config = config
|
||||
self.no_db = self.config.arguments['no_db']
|
||||
if not self.no_db:
|
||||
self.db = DB(config, prefix="/mac")
|
||||
|
||||
self.prefix = 0x420000000000
|
||||
self._number = 0 # Not set by default
|
||||
|
||||
@staticmethod
|
||||
def validate_mac(mac):
|
||||
if not re.match(r'([0-9A-F]{2}[-:]){5}[0-9A-F]{2}$', mac, re.I):
|
||||
raise UncloudException("Not a valid mac address: %s" % mac)
|
||||
else:
|
||||
return True
|
||||
|
||||
def last_used_index(self):
|
||||
if not self.no_db:
|
||||
value = self.db.get("last_used_index")
|
||||
if not value:
|
||||
self.db.set("last_used_index", "0")
|
||||
value = self.db.get("last_used_index")
|
||||
|
||||
else:
|
||||
value = "0"
|
||||
|
||||
return int(value)
|
||||
|
||||
def last_used_mac(self):
|
||||
return self.int_to_mac(self.prefix + self.last_used_index())
|
||||
|
||||
def to_colon_format(self):
|
||||
b = self._number.to_bytes(6, byteorder="big")
|
||||
return ':'.join(format(s, '02x') for s in b)
|
||||
|
||||
def to_str_format(self):
|
||||
b = self._number.to_bytes(6, byteorder="big")
|
||||
return ''.join(format(s, '02x') for s in b)
|
||||
|
||||
def create(self):
|
||||
last_number = self.last_used_index()
|
||||
|
||||
if last_number == int('0xffffffff', 16):
|
||||
raise UncloudException("Exhausted all possible mac addresses - try to free some")
|
||||
|
||||
next_number = last_number + 1
|
||||
self._number = self.prefix + next_number
|
||||
|
||||
#next_number_string = "{:012x}".format(next_number)
|
||||
#next_mac = self.int_to_mac(next_mac_number)
|
||||
# db_entry = {}
|
||||
# db_entry['vm_uuid'] = vmuuid
|
||||
# db_entry['index'] = next_number
|
||||
# db_entry['mac_address'] = next_mac
|
||||
|
||||
# should be one transaction
|
||||
# self.db.increment("last_used_index")
|
||||
# self.db.set("used/{}".format(next_mac),
|
||||
# db_entry, as_json=True)
|
||||
|
||||
def __int__(self):
|
||||
return self._number
|
||||
|
||||
def __repr__(self):
|
||||
return self.to_str_format()
|
||||
|
||||
def __str__(self):
|
||||
return self.to_colon_format()
|
||||
186
uncloud_etcd_based/uncloud/hack/main.py
Normal file
186
uncloud_etcd_based/uncloud/hack/main.py
Normal file
|
|
@ -0,0 +1,186 @@
|
|||
import argparse
|
||||
import logging
|
||||
import re
|
||||
|
||||
import ldap3
|
||||
|
||||
|
||||
from uncloud.hack.vm import VM
|
||||
from uncloud.hack.host import Host
|
||||
from uncloud.hack.config import Config
|
||||
from uncloud.hack.mac import MAC
|
||||
from uncloud.hack.net import VXLANBridge, DNSRA
|
||||
|
||||
from uncloud import UncloudException
|
||||
from uncloud.hack.product import ProductOrder
|
||||
|
||||
arg_parser = argparse.ArgumentParser('hack', add_help=False)
|
||||
#description="Commands that are unfinished - use at own risk")
|
||||
arg_parser.add_argument('--last-used-mac', action='store_true')
|
||||
arg_parser.add_argument('--get-new-mac', action='store_true')
|
||||
|
||||
arg_parser.add_argument('--init-network', help="Initialise networking", action='store_true')
|
||||
arg_parser.add_argument('--create-vxlan', help="Initialise networking", action='store_true')
|
||||
arg_parser.add_argument('--network', help="/64 IPv6 network")
|
||||
arg_parser.add_argument('--vxlan-uplink-device', help="The VXLAN underlay device, i.e. eth0")
|
||||
arg_parser.add_argument('--vni', help="VXLAN ID (decimal)", type=int)
|
||||
arg_parser.add_argument('--run-dns-ra', action='store_true',
|
||||
help="Provide router advertisements and DNS resolution via dnsmasq")
|
||||
arg_parser.add_argument('--use-sudo', help="Use sudo for command requiring root!", action='store_true')
|
||||
|
||||
arg_parser.add_argument('--create-vm', action='store_true')
|
||||
arg_parser.add_argument('--destroy-vm', action='store_true')
|
||||
arg_parser.add_argument('--get-vm-status', action='store_true')
|
||||
arg_parser.add_argument('--get-vm-vnc', action='store_true')
|
||||
arg_parser.add_argument('--list-vms', action='store_true')
|
||||
arg_parser.add_argument('--memory', help="Size of memory (GB)", type=int, default=2)
|
||||
arg_parser.add_argument('--cores', help="Amount of CPU cores", type=int, default=1)
|
||||
arg_parser.add_argument('--image', help="Path (under hackprefix) to OS image")
|
||||
|
||||
arg_parser.add_argument('--image-format', help="Image format: qcow2 or raw", choices=['raw', 'qcow2'])
|
||||
arg_parser.add_argument('--uuid', help="VM UUID")
|
||||
|
||||
arg_parser.add_argument('--no-db', help="Disable connection to etcd. For local testing only!", action='store_true')
|
||||
arg_parser.add_argument('--hackprefix', help="hackprefix, if you need it you know it (it's where the iso is located and ifup/down.sh")
|
||||
|
||||
# order based commands => later to be shifted below "order"
|
||||
arg_parser.add_argument('--order', action='store_true')
|
||||
arg_parser.add_argument('--list-orders', help="List all orders", action='store_true')
|
||||
arg_parser.add_argument('--filter-order-key', help="Which key to filter on")
|
||||
arg_parser.add_argument('--filter-order-regexp', help="Which regexp the value should match")
|
||||
|
||||
arg_parser.add_argument('--process-orders', help="Process all (pending) orders", action='store_true')
|
||||
|
||||
arg_parser.add_argument('--product', choices=["dualstack-vm"])
|
||||
arg_parser.add_argument('--os-image-name', help="Name of OS image (successor to --image)")
|
||||
arg_parser.add_argument('--os-image-size', help="Size of OS image in GB", type=int, default=10)
|
||||
|
||||
arg_parser.add_argument('--username')
|
||||
arg_parser.add_argument('--password')
|
||||
|
||||
arg_parser.add_argument('--api', help="Run the API")
|
||||
arg_parser.add_argument('--mode',
|
||||
choices=["direct", "api", "client"],
|
||||
default="client",
|
||||
help="Directly manipulate etcd, spawn the API server or behave as a client")
|
||||
|
||||
|
||||
arg_parser.add_argument('--add-vm-host', help="Add a host that can run VMs")
|
||||
arg_parser.add_argument('--list-vm-hosts', action='store_true')
|
||||
|
||||
arg_parser.add_argument('--max-cores-per-vm')
|
||||
arg_parser.add_argument('--max-cores-total')
|
||||
arg_parser.add_argument('--max-memory-in-gb')
|
||||
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
def authenticate(username, password, totp_token=None):
|
||||
server = ldap3.Server("ldaps://ldap1.ungleich.ch")
|
||||
dn = "uid={},ou=customer,dc=ungleich,dc=ch".format(username)
|
||||
|
||||
log.debug("LDAP: connecting to {} as {}".format(server, dn))
|
||||
|
||||
try:
|
||||
conn = ldap3.Connection(server, dn, password, auto_bind=True)
|
||||
except ldap3.core.exceptions.LDAPBindError as e:
|
||||
raise UncloudException("Credentials not verified by LDAP server: {}".format(e))
|
||||
|
||||
|
||||
|
||||
def order(config):
|
||||
for required_arg in [ 'product', 'username', 'password' ]:
|
||||
if not config.arguments[required_arg]:
|
||||
raise UncloudException("Missing required argument: {}".format(required_arg))
|
||||
|
||||
if config.arguments['product'] == 'dualstack-vm':
|
||||
for required_arg in [ 'cores', 'memory', 'os_image_name', 'os_image_size' ]:
|
||||
if not config.arguments[required_arg]:
|
||||
raise UncloudException("Missing required argument: {}".format(required_arg))
|
||||
|
||||
log.debug(config.arguments)
|
||||
authenticate(config.arguments['username'], config.arguments['password'])
|
||||
|
||||
# create DB entry for VM
|
||||
vm = VM(config)
|
||||
return vm.product.place_order(owner=config.arguments['username'])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def main(arguments):
|
||||
config = Config(arguments)
|
||||
|
||||
if arguments['add_vm_host']:
|
||||
h = Host(config)
|
||||
h.cmdline_add_host()
|
||||
|
||||
if arguments['list_vm_hosts']:
|
||||
h = Host(config)
|
||||
|
||||
for host in h.list_hosts(filter_key=arguments['filter_order_key'],
|
||||
filter_regexp=arguments['filter_order_regexp']):
|
||||
print("Host {}: {}".format(host.db_entry['uuid'], host.db_entry))
|
||||
|
||||
if arguments['order']:
|
||||
print("Created order: {}".format(order(config)))
|
||||
|
||||
if arguments['list_orders']:
|
||||
p = ProductOrder(config)
|
||||
for product_order in p.list_orders(filter_key=arguments['filter_order_key'],
|
||||
filter_regexp=arguments['filter_order_regexp']):
|
||||
print("Order {}: {}".format(product_order.db_entry['uuid'], product_order.db_entry))
|
||||
|
||||
if arguments['process_orders']:
|
||||
p = ProductOrder(config)
|
||||
p.process_orders()
|
||||
|
||||
if arguments['create_vm']:
|
||||
vm = VM(config)
|
||||
vm.create()
|
||||
|
||||
if arguments['destroy_vm']:
|
||||
vm = VM(config)
|
||||
vm.stop()
|
||||
|
||||
if arguments['get_vm_status']:
|
||||
vm = VM(config)
|
||||
vm.status()
|
||||
|
||||
if arguments['get_vm_vnc']:
|
||||
vm = VM(config)
|
||||
vm.vnc_addr()
|
||||
|
||||
if arguments['list_vms']:
|
||||
vm = VM(config)
|
||||
vm.list()
|
||||
|
||||
if arguments['last_used_mac']:
|
||||
m = MAC(config)
|
||||
print(m.last_used_mac())
|
||||
|
||||
if arguments['get_new_mac']:
|
||||
print(MAC(config).get_next())
|
||||
|
||||
#if arguments['init_network']:
|
||||
if arguments['create_vxlan']:
|
||||
if not arguments['network'] or not arguments['vni'] or not arguments['vxlan_uplink_device']:
|
||||
raise UncloudException("Initialising the network requires an IPv6 network and a VNI. You can use fd00::/64 and vni=1 for testing (non production!)")
|
||||
vb = VXLANBridge(vni=arguments['vni'],
|
||||
route=arguments['network'],
|
||||
uplinkdev=arguments['vxlan_uplink_device'],
|
||||
use_sudo=arguments['use_sudo'])
|
||||
vb._setup_vxlan()
|
||||
vb._setup_bridge()
|
||||
vb._add_vxlan_to_bridge()
|
||||
vb._route_network()
|
||||
|
||||
if arguments['run_dns_ra']:
|
||||
if not arguments['network'] or not arguments['vni']:
|
||||
raise UncloudException("Providing DNS/RAs requires a /64 IPv6 network and a VNI. You can use fd00::/64 and vni=1 for testing (non production!)")
|
||||
|
||||
dnsra = DNSRA(route=arguments['network'],
|
||||
vni=arguments['vni'],
|
||||
use_sudo=arguments['use_sudo'])
|
||||
dnsra._setup_dnsmasq()
|
||||
116
uncloud_etcd_based/uncloud/hack/net.py
Normal file
116
uncloud_etcd_based/uncloud/hack/net.py
Normal file
|
|
@ -0,0 +1,116 @@
|
|||
import subprocess
|
||||
import ipaddress
|
||||
import logging
|
||||
|
||||
|
||||
from uncloud import UncloudException
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class VXLANBridge(object):
|
||||
cmd_create_vxlan = "{sudo}ip -6 link add {vxlandev} type vxlan id {vni_dec} dstport 4789 group {multicast_address} dev {uplinkdev} ttl 5"
|
||||
cmd_up_dev = "{sudo}ip link set {dev} up"
|
||||
cmd_create_bridge="{sudo}ip link add {bridgedev} type bridge"
|
||||
cmd_add_to_bridge="{sudo}ip link set {vxlandev} master {bridgedev} up"
|
||||
cmd_add_addr="{sudo}ip addr add {ip} dev {bridgedev}"
|
||||
cmd_add_route_dev="{sudo}ip route add {route} dev {bridgedev}"
|
||||
|
||||
# VXLAN ids are at maximum 24 bit - use a /104
|
||||
multicast_network = ipaddress.IPv6Network("ff05::/104")
|
||||
max_vni = (2**24)-1
|
||||
|
||||
def __init__(self,
|
||||
vni,
|
||||
uplinkdev,
|
||||
route=None,
|
||||
use_sudo=False):
|
||||
self.config = {}
|
||||
|
||||
if vni > self.max_vni:
|
||||
raise UncloudException("VNI must be in the range of 0 .. {}".format(self.max_vni))
|
||||
|
||||
if use_sudo:
|
||||
self.config['sudo'] = 'sudo '
|
||||
else:
|
||||
self.config['sudo'] = ''
|
||||
|
||||
self.config['vni_dec'] = vni
|
||||
self.config['vni_hex'] = "{:x}".format(vni)
|
||||
self.config['multicast_address'] = self.multicast_network[vni]
|
||||
|
||||
self.config['route_network'] = ipaddress.IPv6Network(route)
|
||||
self.config['route'] = route
|
||||
|
||||
self.config['uplinkdev'] = uplinkdev
|
||||
self.config['vxlandev'] = "vx{}".format(self.config['vni_hex'])
|
||||
self.config['bridgedev'] = "br{}".format(self.config['vni_hex'])
|
||||
|
||||
|
||||
def setup_networking(self):
|
||||
pass
|
||||
|
||||
def _setup_vxlan(self):
|
||||
self._execute_cmd(self.cmd_create_vxlan)
|
||||
self._execute_cmd(self.cmd_up_dev, dev=self.config['vxlandev'])
|
||||
|
||||
def _setup_bridge(self):
|
||||
self._execute_cmd(self.cmd_create_bridge)
|
||||
self._execute_cmd(self.cmd_up_dev, dev=self.config['bridgedev'])
|
||||
|
||||
def _route_network(self):
|
||||
self._execute_cmd(self.cmd_add_route_dev)
|
||||
|
||||
def _add_vxlan_to_bridge(self):
|
||||
self._execute_cmd(self.cmd_add_to_bridge)
|
||||
|
||||
def _execute_cmd(self, cmd_string, **kwargs):
|
||||
cmd = cmd_string.format(**self.config, **kwargs)
|
||||
log.info("Executing: {}".format(cmd))
|
||||
subprocess.run(cmd.split())
|
||||
|
||||
class ManagementBridge(VXLANBridge):
|
||||
pass
|
||||
|
||||
|
||||
class DNSRA(object):
|
||||
# VXLAN ids are at maximum 24 bit
|
||||
max_vni = (2**24)-1
|
||||
|
||||
|
||||
# Command to start dnsmasq
|
||||
cmd_start_dnsmasq="{sudo}dnsmasq --interface={bridgedev} --bind-interfaces --dhcp-range={route},ra-only,infinite --enable-ra --no-daemon"
|
||||
|
||||
def __init__(self,
|
||||
vni,
|
||||
route=None,
|
||||
use_sudo=False):
|
||||
self.config = {}
|
||||
|
||||
if vni > self.max_vni:
|
||||
raise UncloudException("VNI must be in the range of 0 .. {}".format(self.max_vni))
|
||||
|
||||
if use_sudo:
|
||||
self.config['sudo'] = 'sudo '
|
||||
else:
|
||||
self.config['sudo'] = ''
|
||||
|
||||
#TODO: remove if not needed
|
||||
#self.config['vni_dec'] = vni
|
||||
self.config['vni_hex'] = "{:x}".format(vni)
|
||||
|
||||
# dnsmasq only wants the network without the prefix, therefore, cut it off
|
||||
self.config['route'] = ipaddress.IPv6Network(route).network_address
|
||||
self.config['bridgedev'] = "br{}".format(self.config['vni_hex'])
|
||||
|
||||
def _setup_dnsmasq(self):
|
||||
self._execute_cmd(self.cmd_start_dnsmasq)
|
||||
|
||||
def _execute_cmd(self, cmd_string, **kwargs):
|
||||
cmd = cmd_string.format(**self.config, **kwargs)
|
||||
log.info("Executing: {}".format(cmd))
|
||||
print("Executing: {}".format(cmd))
|
||||
subprocess.run(cmd.split())
|
||||
|
||||
class Firewall(object):
|
||||
pass
|
||||
94
uncloud_etcd_based/uncloud/hack/nftables.conf
Normal file
94
uncloud_etcd_based/uncloud/hack/nftables.conf
Normal file
|
|
@ -0,0 +1,94 @@
|
|||
flush ruleset
|
||||
|
||||
table bridge filter {
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority 0;
|
||||
policy accept;
|
||||
ibrname br100 jump netpublic
|
||||
}
|
||||
chain netpublic {
|
||||
icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } log
|
||||
}
|
||||
}
|
||||
|
||||
table ip6 filter {
|
||||
chain forward {
|
||||
type filter hook forward priority 0;
|
||||
|
||||
# this would be nice...
|
||||
policy drop;
|
||||
|
||||
ct state established,related accept;
|
||||
|
||||
}
|
||||
|
||||
chain prerouting {
|
||||
type filter hook prerouting priority 0;
|
||||
policy accept;
|
||||
|
||||
# not supporting in here!
|
||||
|
||||
|
||||
iifname vmXXXX jump vmXXXX
|
||||
iifname vmYYYY jump vmYYYY
|
||||
|
||||
iifname brXX jump brXX
|
||||
|
||||
iifname vxlan100 jump vxlan100
|
||||
iifname br100 jump br100
|
||||
}
|
||||
|
||||
# 1. Rules per VM (names: vmXXXXX?
|
||||
# 2. Rules per network (names: vxlanXXXX, what about non vxlan?)
|
||||
# 3. Rules per bridge:
|
||||
# vxlanXX is inside brXX
|
||||
# This is effectively a network filter
|
||||
# 4. Kill all malicous traffic:
|
||||
# - router advertisements from VMs in which they should not announce RAs
|
||||
|
||||
|
||||
|
||||
chain vxlan100 {
|
||||
icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } log
|
||||
}
|
||||
chain br100 {
|
||||
icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } log
|
||||
}
|
||||
|
||||
chain netpublic {
|
||||
# drop router advertisements that don't come from us
|
||||
iifname != vxlanpublic icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } drop
|
||||
# icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } drop
|
||||
|
||||
}
|
||||
|
||||
# This vlan
|
||||
chain brXX {
|
||||
ip6 saddr != 2001:db8:1::/64 drop;
|
||||
}
|
||||
|
||||
chain vmXXXX {
|
||||
ether saddr != 00:0f:54:0c:11:04 drop;
|
||||
ip6 saddr != 2001:db8:1:000f::540c:11ff:fe04 drop;
|
||||
jump drop_from_vm_without_ipam
|
||||
}
|
||||
|
||||
chain net_2a0ae5c05something {
|
||||
|
||||
|
||||
}
|
||||
|
||||
chain drop_from_vm_without_ipam {
|
||||
|
||||
}
|
||||
|
||||
chain vmYYYY {
|
||||
ether saddr != 00:0f:54:0c:11:05 drop;
|
||||
jump drop_from_vm_with_ipam
|
||||
}
|
||||
|
||||
# Drop stuff from every VM
|
||||
chain drop_from_vm_with_ipam {
|
||||
icmpv6 type {nd-router-solicit, nd-router-advert, nd-neighbor-solicit, nd-neighbor-advert, nd-redirect } drop
|
||||
}
|
||||
}
|
||||
206
uncloud_etcd_based/uncloud/hack/product.py
Executable file
206
uncloud_etcd_based/uncloud/hack/product.py
Executable file
|
|
@ -0,0 +1,206 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
|
||||
#
|
||||
# This file is part of uncloud.
|
||||
#
|
||||
# uncloud is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# uncloud is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
import json
|
||||
import uuid
|
||||
import logging
|
||||
import re
|
||||
import importlib
|
||||
|
||||
from uncloud import UncloudException
|
||||
from uncloud.hack.db import DB, db_logentry
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
|
||||
class ProductOrder(object):
|
||||
def __init__(self, config, product_entry=None, db_entry=None):
|
||||
self.config = config
|
||||
self.db = DB(self.config, prefix="/orders")
|
||||
self.db_entry = {}
|
||||
self.db_entry["product"] = product_entry
|
||||
|
||||
# Overwrite if we are loading an existing product order
|
||||
if db_entry:
|
||||
self.db_entry = db_entry
|
||||
|
||||
# FIXME: this should return a list of our class!
|
||||
def list_orders(self, filter_key=None, filter_regexp=None):
|
||||
for entry in self.db.list_and_filter("", filter_key, filter_regexp):
|
||||
yield self.__class__(self.config, db_entry=entry)
|
||||
|
||||
|
||||
def set_required_values(self):
|
||||
"""Set values that are required to make the db entry valid"""
|
||||
if not "uuid" in self.db_entry:
|
||||
self.db_entry["uuid"] = str(uuid.uuid4())
|
||||
if not "status" in self.db_entry:
|
||||
self.db_entry["status"] = "NEW"
|
||||
if not "owner" in self.db_entry:
|
||||
self.db_entry["owner"] = "UNKNOWN"
|
||||
if not "log" in self.db_entry:
|
||||
self.db_entry["log"] = []
|
||||
if not "db_version" in self.db_entry:
|
||||
self.db_entry["db_version"] = 1
|
||||
|
||||
def validate_status(self):
|
||||
if "status" in self.db_entry:
|
||||
if self.db_entry["status"] in [ "NEW",
|
||||
"SCHEDULED",
|
||||
"CREATED_ACTIVE",
|
||||
"CANCELLED",
|
||||
"REJECTED" ]:
|
||||
return False
|
||||
return True
|
||||
|
||||
def order(self):
|
||||
self.set_required_values()
|
||||
if not self.db_entry["status"] == "NEW":
|
||||
raise UncloudException("Cannot re-order same order. Status: {}".format(self.db_entry["status"]))
|
||||
self.db.set(self.db_entry["uuid"], self.db_entry, as_json=True)
|
||||
|
||||
return self.db_entry["uuid"]
|
||||
|
||||
def process_orders(self):
|
||||
"""processing orders can be done stand alone on server side"""
|
||||
for order in self.list_orders():
|
||||
if order.db_entry["status"] == "NEW":
|
||||
log.info("Handling new order: {}".format(order))
|
||||
|
||||
# FIXME: these all should be a transactions! -> fix concurrent access! !
|
||||
if not "log" in order.db_entry:
|
||||
order.db_entry['log'] = []
|
||||
|
||||
is_valid = True
|
||||
# Verify the order entry
|
||||
for must_attribute in [ "owner", "product" ]:
|
||||
if not must_attribute in order.db_entry:
|
||||
message = "Missing {} entry in order, rejecting order".format(must_attribute)
|
||||
log.info("Rejecting order {}: {}".format(order.db_entry["uuid"], message))
|
||||
|
||||
order.db_entry['log'].append(db_logentry(message))
|
||||
order.db_entry['status'] = "REJECTED"
|
||||
self.db.set(order.db_entry['uuid'], order.db_entry, as_json=True)
|
||||
|
||||
is_valid = False
|
||||
|
||||
# Rejected the order
|
||||
if not is_valid:
|
||||
continue
|
||||
|
||||
# Verify the product entry
|
||||
for must_attribute in [ "python_product_class", "python_product_module" ]:
|
||||
if not must_attribute in order.db_entry['product']:
|
||||
message = "Missing {} entry in product of order, rejecting order".format(must_attribute)
|
||||
log.info("Rejecting order {}: {}".format(order.db_entry["uuid"], message))
|
||||
|
||||
order.db_entry['log'].append(db_logentry(message))
|
||||
order.db_entry['status'] = "REJECTED"
|
||||
self.db.set(order.db_entry['uuid'], order.db_entry, as_json=True)
|
||||
|
||||
is_valid = False
|
||||
|
||||
# Rejected the order
|
||||
if not is_valid:
|
||||
continue
|
||||
|
||||
print(order.db_entry["product"]["python_product_class"])
|
||||
|
||||
# Create the product
|
||||
m = importlib.import_module(order.db_entry["product"]["python_product_module"])
|
||||
c = getattr(m, order.db_entry["product"]["python_product_class"])
|
||||
|
||||
product = c(config, db_entry=order.db_entry["product"])
|
||||
|
||||
# STOPPED
|
||||
product.create_product()
|
||||
|
||||
order.db_entry['status'] = "SCHEDULED"
|
||||
self.db.set(order.db_entry['uuid'], order.db_entry, as_json=True)
|
||||
|
||||
|
||||
|
||||
def __str__(self):
|
||||
return str(self.db_entry)
|
||||
|
||||
class Product(object):
|
||||
def __init__(self,
|
||||
config,
|
||||
product_name,
|
||||
product_class,
|
||||
db_entry=None):
|
||||
self.config = config
|
||||
self.db = DB(self.config, prefix="/orders")
|
||||
|
||||
self.db_entry = {}
|
||||
self.db_entry["product_name"] = product_name
|
||||
self.db_entry["python_product_class"] = product_class.__qualname__
|
||||
self.db_entry["python_product_module"] = product_class.__module__
|
||||
self.db_entry["db_version"] = 1
|
||||
self.db_entry["log"] = []
|
||||
self.db_entry["features"] = {}
|
||||
|
||||
# Existing product? Read in db_entry
|
||||
if db_entry:
|
||||
self.db_entry = db_entry
|
||||
|
||||
self.valid_periods = [ "per_year", "per_month", "per_week",
|
||||
"per_day", "per_hour",
|
||||
"per_minute", "per_second" ]
|
||||
|
||||
def define_feature(self,
|
||||
name,
|
||||
one_time_price,
|
||||
recurring_price,
|
||||
recurring_period,
|
||||
minimum_period):
|
||||
|
||||
self.db_entry['features'][name] = {}
|
||||
self.db_entry['features'][name]['one_time_price'] = one_time_price
|
||||
self.db_entry['features'][name]['recurring_price'] = recurring_price
|
||||
|
||||
if not recurring_period in self.valid_periods:
|
||||
raise UncloudException("Invalid recurring period: {}".format(recurring_period))
|
||||
|
||||
self.db_entry['features'][name]['recurring_period'] = recurring_period
|
||||
|
||||
if not minimum_period in self.valid_periods:
|
||||
raise UncloudException("Invalid recurring period: {}".format(recurring_period))
|
||||
|
||||
recurring_index = self.valid_periods.index(recurring_period)
|
||||
minimum_index = self.valid_periods.index(minimum_period)
|
||||
|
||||
if minimum_index < recurring_index:
|
||||
raise UncloudException("Minimum period for product '{}' feature '{}' must be shorter or equal than/as recurring period: {} > {}".format(self.db_entry['product_name'], name, minimum_period, recurring_period))
|
||||
|
||||
self.db_entry['features'][name]['minimum_period'] = minimum_period
|
||||
|
||||
|
||||
def validate_product(self):
|
||||
for feature in self.db_entry['features']:
|
||||
pass
|
||||
|
||||
def place_order(self, owner):
|
||||
""" Schedule creating the product in etcd """
|
||||
order = ProductOrder(self.config, product_entry=self.db_entry)
|
||||
order.db_entry["owner"] = owner
|
||||
return order.order()
|
||||
|
||||
def __str__(self):
|
||||
return json.dumps(self.db_entry)
|
||||
8
uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api
Normal file
8
uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-api
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/sbin/openrc-run
|
||||
|
||||
name="$RC_SVCNAME"
|
||||
pidfile="/var/run/${name}.pid"
|
||||
command="$(which pipenv)"
|
||||
command_args="run python ucloud.py api"
|
||||
command_background="true"
|
||||
directory="/root/ucloud"
|
||||
8
uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host
Normal file
8
uncloud_etcd_based/uncloud/hack/rc-scripts/ucloud-host
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
#!/sbin/openrc-run
|
||||
|
||||
name="$RC_SVCNAME"
|
||||
pidfile="/var/run/${name}.pid"
|
||||
command="$(which pipenv)"
|
||||
command_args="run python ucloud.py host ${HOSTNAME}"
|
||||
command_background="true"
|
||||
directory="/root/ucloud"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/sbin/openrc-run
|
||||
|
||||
name="$RC_SVCNAME"
|
||||
pidfile="/var/run/${name}.pid"
|
||||
command="$(which pipenv)"
|
||||
command_args="run python ucloud.py metadata"
|
||||
command_background="true"
|
||||
directory="/root/ucloud"
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
#!/sbin/openrc-run
|
||||
|
||||
name="$RC_SVCNAME"
|
||||
pidfile="/var/run/${name}.pid"
|
||||
command="$(which pipenv)"
|
||||
command_args="run python ucloud.py scheduler"
|
||||
command_background="true"
|
||||
directory="/root/ucloud"
|
||||
26
uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host
Normal file
26
uncloud_etcd_based/uncloud/hack/uncloud-hack-init-host
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
id=100
|
||||
rawdev=eth0
|
||||
|
||||
# create vxlan
|
||||
ip -6 link add vxlan${id} type vxlan \
|
||||
id ${id} \
|
||||
dstport 4789 \
|
||||
group ff05::${id} \
|
||||
dev ${rawdev} \
|
||||
ttl 5
|
||||
|
||||
ip link set vxlan${id} up
|
||||
|
||||
# create bridge
|
||||
ip link set vxlan${id} up
|
||||
ip link set br${id} up
|
||||
|
||||
# Add vxlan into bridge
|
||||
ip link set vxlan${id} master br${id}
|
||||
|
||||
|
||||
# useradd -m uncloud
|
||||
# [18:05] tablett.place10:~# id uncloud
|
||||
# uid=1000(uncloud) gid=1000(uncloud) groups=1000(uncloud),34(kvm),36(qemu)
|
||||
# apk add qemu-system-x86_64
|
||||
# also needs group netdev
|
||||
25
uncloud_etcd_based/uncloud/hack/uncloud-run-vm
Normal file
25
uncloud_etcd_based/uncloud/hack/uncloud-run-vm
Normal file
|
|
@ -0,0 +1,25 @@
|
|||
#!/bin/sh
|
||||
|
||||
if [ $# -ne 1 ]; then
|
||||
echo $0 vmid
|
||||
exit 1
|
||||
fi
|
||||
|
||||
id=$1; shift
|
||||
|
||||
memory=512
|
||||
macaddress=02:00:b9:cb:70:${id}
|
||||
netname=net${id}-1
|
||||
|
||||
qemu-system-x86_64 \
|
||||
-name uncloud-${id} \
|
||||
-accel kvm \
|
||||
-m ${memory} \
|
||||
-smp 2,sockets=2,cores=1,threads=1 \
|
||||
-device virtio-net-pci,netdev=net0,mac=$macaddress \
|
||||
-netdev tap,id=net0,ifname=${netname},script=no,downscript=no \
|
||||
-vnc [::]:0
|
||||
|
||||
# To be changed:
|
||||
# -vnc to unix path
|
||||
# or -spice
|
||||
193
uncloud_etcd_based/uncloud/hack/vm.py
Executable file
193
uncloud_etcd_based/uncloud/hack/vm.py
Executable file
|
|
@ -0,0 +1,193 @@
|
|||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
#
|
||||
# 2020 Nico Schottelius (nico.schottelius at ungleich.ch)
|
||||
#
|
||||
# This file is part of uncloud.
|
||||
#
|
||||
# uncloud is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU General Public License as published by
|
||||
# the Free Software Foundation, either version 3 of the License, or
|
||||
# (at your option) any later version.
|
||||
#
|
||||
# uncloud is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU General Public License
|
||||
# along with uncloud. If not, see <http://www.gnu.org/licenses/>.
|
||||
|
||||
# This module is directly called from the hack module, and can be used as follow:
|
||||
#
|
||||
# Create a new VM with default CPU/Memory. The path of the image file is relative to $hackprefix.
|
||||
# `uncloud hack --hackprefix /tmp/hackcloud --create-vm --image mysuperimage.qcow2`
|
||||
#
|
||||
# List running VMs (returns a list of UUIDs).
|
||||
# `uncloud hack --hackprefix /tmp/hackcloud --list-vms
|
||||
#
|
||||
# Get VM status:
|
||||
# `uncloud hack --hackprefix /tmp/hackcloud --get-vm-status --uuid my-vm-uuid`
|
||||
#
|
||||
# Stop a VM:
|
||||
# `uncloud hack --hackprefix /tmp/hackcloud --destroy-vm --uuid my-vm-uuid`
|
||||
# ``
|
||||
|
||||
import subprocess
|
||||
import uuid
|
||||
import os
|
||||
import logging
|
||||
|
||||
from uncloud.hack.db import DB
|
||||
from uncloud.hack.mac import MAC
|
||||
from uncloud.vmm import VMM
|
||||
from uncloud.hack.product import Product
|
||||
|
||||
log = logging.getLogger(__name__)
|
||||
log.setLevel(logging.DEBUG)
|
||||
|
||||
class VM(object):
|
||||
def __init__(self, config, db_entry=None):
|
||||
self.config = config
|
||||
|
||||
#TODO: Enable etcd lookup
|
||||
self.no_db = self.config.arguments['no_db']
|
||||
if not self.no_db:
|
||||
self.db = DB(self.config, prefix="/vm")
|
||||
|
||||
if db_entry:
|
||||
self.db_entry = db_entry
|
||||
|
||||
# General CLI arguments.
|
||||
self.hackprefix = self.config.arguments['hackprefix']
|
||||
self.uuid = self.config.arguments['uuid']
|
||||
self.memory = self.config.arguments['memory'] or '1024M'
|
||||
self.cores = self.config.arguments['cores'] or 1
|
||||
|
||||
if self.config.arguments['image']:
|
||||
self.image = os.path.join(self.hackprefix, self.config.arguments['image'])
|
||||
else:
|
||||
self.image = None
|
||||
|
||||
if self.config.arguments['image_format']:
|
||||
self.image_format=self.config.arguments['image_format']
|
||||
else:
|
||||
self.image_format='qcow2'
|
||||
|
||||
# External components.
|
||||
|
||||
# This one is broken:
|
||||
# TypeError: expected str, bytes or os.PathLike object, not NoneType
|
||||
# Fix before re-enabling
|
||||
# self.vmm = VMM(vmm_backend=self.hackprefix)
|
||||
self.mac = MAC(self.config)
|
||||
|
||||
# Harcoded & generated values.
|
||||
self.owner = 'uncloud'
|
||||
self.accel = 'kvm'
|
||||
self.threads = 1
|
||||
self.ifup = os.path.join(self.hackprefix, "ifup.sh")
|
||||
self.ifdown = os.path.join(self.hackprefix, "ifdown.sh")
|
||||
self.ifname = "uc{}".format(self.mac.to_str_format())
|
||||
|
||||
self.vm = {}
|
||||
|
||||
self.product = Product(config, product_name="dualstack-vm",
|
||||
product_class=self.__class__)
|
||||
self.product.define_feature(name="base",
|
||||
one_time_price=0,
|
||||
recurring_price=9,
|
||||
recurring_period="per_month",
|
||||
minimum_period="per_hour")
|
||||
|
||||
|
||||
self.features = []
|
||||
|
||||
|
||||
def get_qemu_args(self):
|
||||
command = (
|
||||
"-name {owner}-{name}"
|
||||
" -machine pc,accel={accel}"
|
||||
" -drive file={image},format={image_format},if=virtio"
|
||||
" -device virtio-rng-pci"
|
||||
" -m {memory} -smp cores={cores},threads={threads}"
|
||||
" -netdev tap,id=netmain,script={ifup},downscript={ifdown},ifname={ifname}"
|
||||
" -device virtio-net-pci,netdev=netmain,id=net0,mac={mac}"
|
||||
).format(
|
||||
owner=self.owner, name=self.uuid,
|
||||
accel=self.accel,
|
||||
image=self.image, image_format=self.image_format,
|
||||
memory=self.memory, cores=self.cores, threads=self.threads,
|
||||
ifup=self.ifup, ifdown=self.ifdown, ifname=self.ifname,
|
||||
mac=self.mac
|
||||
)
|
||||
|
||||
return command.split(" ")
|
||||
|
||||
def create_product(self):
|
||||
"""Find a VM host and schedule on it"""
|
||||
pass
|
||||
|
||||
def create(self):
|
||||
# New VM: new UUID, new MAC.
|
||||
self.uuid = str(uuid.uuid4())
|
||||
self.mac=MAC(self.config)
|
||||
self.mac.create()
|
||||
|
||||
qemu_args = self.get_qemu_args()
|
||||
log.debug("QEMU args passed to VMM: {}".format(qemu_args))
|
||||
self.vmm.start(
|
||||
uuid=self.uuid,
|
||||
migration=False,
|
||||
*qemu_args
|
||||
)
|
||||
|
||||
|
||||
self.mac.create()
|
||||
self.vm['mac'] = self.mac
|
||||
self.vm['ifname'] = "uc{}".format(self.mac.__repr__())
|
||||
|
||||
# FIXME: TODO: turn this into a string and THEN
|
||||
# .split() it later -- easier for using .format()
|
||||
#self.vm['commandline'] = [ "{}".format(self.sudo),
|
||||
self.vm['commandline'] = "{sudo}{qemu} -name uncloud-{uuid} -machine pc,accel={accel} -m {memory} -smp {cores} -uuid {uuid} -drive file={os_image},media=cdrom -netdev tap,id=netmain,script={ifup},downscript={ifdown},ifname={ifname} -device virtio-net-pci,netdev=netmain,id=net0,mac={mac}"
|
||||
# self.vm['commandline'] = [ "{}".format(self.sudo),
|
||||
# "{}".format(self.qemu),
|
||||
# "-name", "uncloud-{}".format(self.vm['uuid']),
|
||||
# "-machine", "pc,accel={}".format(self.accel),
|
||||
# "-m", "{}".format(self.vm['memory']),
|
||||
# "-smp", "{}".format(self.vm['cores']),
|
||||
# "-uuid", "{}".format(self.vm['uuid']),
|
||||
# "-drive", "file={},media=cdrom".format(self.vm['os_image']),
|
||||
# "-netdev", "tap,id=netmain,script={},downscript={},ifname={}".format(self.ifup, self.ifdown, self.vm['ifname']),
|
||||
# "-device", "virtio-net-pci,netdev=netmain,id=net0,mac={}".format(self.vm['mac'])
|
||||
# ]
|
||||
|
||||
def _execute_cmd(self, cmd_string, **kwargs):
|
||||
cmd = cmd_string.format(**self.vm, **kwargs)
|
||||
log.info("Executing: {}".format(cmd))
|
||||
subprocess.run(cmd.split())
|
||||
|
||||
def stop(self):
|
||||
if not self.uuid:
|
||||
print("Please specific an UUID with the --uuid flag.")
|
||||
exit(1)
|
||||
|
||||
self.vmm.stop(self.uuid)
|
||||
|
||||
def status(self):
|
||||
if not self.uuid:
|
||||
print("Please specific an UUID with the --uuid flag.")
|
||||
exit(1)
|
||||
|
||||
print(self.vmm.get_status(self.uuid))
|
||||
|
||||
def vnc_addr(self):
|
||||
if not self.uuid:
|
||||
print("Please specific an UUID with the --uuid flag.")
|
||||
exit(1)
|
||||
|
||||
print(self.vmm.get_vnc(self.uuid))
|
||||
|
||||
def list(self):
|
||||
print(self.vmm.discover())
|
||||
Loading…
Add table
Add a link
Reference in a new issue