Separate repo conf, build should now be almost complete
This commit is contained in:
parent
c3b711850e
commit
4c2616887f
5
etc/lxcmgr/repo.json
Normal file
5
etc/lxcmgr/repo.json
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
{
|
||||||
|
"url": "https://repo.spotter.cz/lxc",
|
||||||
|
"user": "",
|
||||||
|
"pwd": ""
|
||||||
|
}
|
@ -8,11 +8,5 @@
|
|||||||
"adminpwd": "${ADMINPWD}",
|
"adminpwd": "${ADMINPWD}",
|
||||||
"domain": "spotter.vm",
|
"domain": "spotter.vm",
|
||||||
"port": "443"
|
"port": "443"
|
||||||
},
|
|
||||||
"packages": {},
|
|
||||||
"repo": {
|
|
||||||
"pwd": "",
|
|
||||||
"url": "https://repo.spotter.cz/lxc",
|
|
||||||
"user": ""
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -44,16 +44,6 @@ parser_container_cleanup.set_defaults(action='container-cleanup')
|
|||||||
parser_container_cleanup.add_argument('container', help='Container name')
|
parser_container_cleanup.add_argument('container', help='Container name')
|
||||||
parser_container_cleanup.add_argument('lxc', nargs=argparse.REMAINDER)
|
parser_container_cleanup.add_argument('lxc', nargs=argparse.REMAINDER)
|
||||||
|
|
||||||
parser_container_create = subparsers_container.add_parser('create')
|
|
||||||
parser_container_create.set_defaults(action='container-create')
|
|
||||||
parser_container_create.add_argument('container', help='Container name')
|
|
||||||
parser_container_create.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
parser_container_destroy = subparsers_container.add_parser('destroy')
|
|
||||||
parser_container_destroy.set_defaults(action='container-destroy')
|
|
||||||
parser_container_destroy.add_argument('container', help='Container name')
|
|
||||||
parser_container_destroy.add_argument('lxc', nargs=argparse.REMAINDER)
|
|
||||||
|
|
||||||
def print_apps(packages):
|
def print_apps(packages):
|
||||||
for app, meta in packages.items():
|
for app, meta in packages.items():
|
||||||
print('{} {}'.format(app, meta['version']))
|
print('{} {}'.format(app, meta['version']))
|
||||||
@ -110,9 +100,3 @@ elif args.action == 'container-prepare':
|
|||||||
elif args.action == 'container-cleanup':
|
elif args.action == 'container-cleanup':
|
||||||
# Used with LXC hooks on container stop
|
# Used with LXC hooks on container stop
|
||||||
lxcmgr.cleanup_container(args.container)
|
lxcmgr.cleanup_container(args.container)
|
||||||
elif args.action == 'container-create':
|
|
||||||
# Used by package installer and builder
|
|
||||||
lxcmgr.register_container(args.container)
|
|
||||||
elif args.action == 'container-destroy':
|
|
||||||
# Used by package installer and builder
|
|
||||||
lxcmgr.unregister_container(args.container)
|
|
||||||
|
@ -9,20 +9,20 @@ from cryptography.hazmat.primitives.asymmetric import ec
|
|||||||
|
|
||||||
from .paths import REPO_SIG_FILE
|
from .paths import REPO_SIG_FILE
|
||||||
|
|
||||||
def verify_signature(file, signature):
|
def verify_signature(public_key_path, input_data, signature_data):
|
||||||
# Verifies ECDSA HMAC SHA512 signature of a file
|
# Verifies ECDSA HMAC SHA512 signature of a file
|
||||||
with open(REPO_SIG_FILE, 'rb') as f:
|
with open(public_key_path, 'rb') as f:
|
||||||
pub_key = serialization.load_pem_public_key(f.read(), default_backend())
|
pub_key = serialization.load_pem_public_key(f.read(), default_backend())
|
||||||
pub_key.verify(signature, file, ec.ECDSA(hashes.SHA512()))
|
pub_key.verify(signature_data, input_data, ec.ECDSA(hashes.SHA512()))
|
||||||
|
|
||||||
def verify_hash(file, expected_hash):
|
def verify_hash(input_path, expected_hash):
|
||||||
# Verifies SHA512 hash of a file against expected hash
|
# Verifies SHA512 hash of a file against expected hash
|
||||||
sha512 = hashlib.sha512()
|
sha512 = hashlib.sha512()
|
||||||
with open(file, 'rb') as f:
|
with open(input_path, 'rb') as f:
|
||||||
while True:
|
while True:
|
||||||
data = f.read(65536)
|
data = f.read(65536)
|
||||||
if not data:
|
if not data:
|
||||||
break
|
break
|
||||||
sha512.update(data)
|
sha512.update(data)
|
||||||
if sha512.hexdigest() != expected_hash:
|
if sha512.hexdigest() != expected_hash:
|
||||||
raise InvalidSignature(file)
|
raise InvalidSignature(input_path)
|
||||||
|
@ -6,7 +6,7 @@ import shutil
|
|||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
from . import flock
|
from . import flock
|
||||||
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_ROOT
|
from .paths import HOSTS_FILE, HOSTS_LOCK, LXC_LOGS, LXC_ROOT, LXC_STORAGE_DIR
|
||||||
from .templates import LXC_CONTAINER
|
from .templates import LXC_CONTAINER
|
||||||
|
|
||||||
def prepare_container(container, layers):
|
def prepare_container(container, layers):
|
||||||
@ -67,6 +67,7 @@ def create_container(container, image):
|
|||||||
def destroy_container(container):
|
def destroy_container(container):
|
||||||
# Remove container configuration and directories
|
# Remove container configuration and directories
|
||||||
shutil.rmtree(os.path.join(LXC_ROOT, container))
|
shutil.rmtree(os.path.join(LXC_ROOT, container))
|
||||||
|
os.unlink(os.path.join(LXC_LOGS, '{}.log'.format(container)))
|
||||||
# Release the IP address
|
# Release the IP address
|
||||||
update_hosts_lease(container, False)
|
update_hosts_lease(container, False)
|
||||||
|
|
||||||
|
@ -2,12 +2,14 @@
|
|||||||
|
|
||||||
# Package manager
|
# Package manager
|
||||||
REPO_CACHE_DIR = '/var/lib/lxcmgr/cache'
|
REPO_CACHE_DIR = '/var/lib/lxcmgr/cache'
|
||||||
|
REPO_CONF_FILE = '/etc/lxcmgr/repo.json'
|
||||||
REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages'
|
REPO_LOCAL_FILE = '/var/lib/lxcmgr/packages'
|
||||||
REPO_LOCK = '/var/lock/lxcmgr-repo.lock'
|
REPO_LOCK = '/var/lock/lxcmgr-repo.lock'
|
||||||
REPO_SIG_FILE = '/var/lib/lxcmgr/packages.pub'
|
REPO_SIG_FILE = '/etc/lxcmgr/packages.pub'
|
||||||
|
|
||||||
# LXC
|
# LXC
|
||||||
HOSTS_FILE = '/etc/hosts'
|
HOSTS_FILE = '/etc/hosts'
|
||||||
HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock'
|
HOSTS_LOCK = '/var/lock/lxcmgr-hosts.lock'
|
||||||
|
LXC_LOGS = '/var/log/lxc'
|
||||||
LXC_ROOT = '/var/lib/lxc'
|
LXC_ROOT = '/var/lib/lxc'
|
||||||
LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage'
|
LXC_STORAGE_DIR = '/var/lib/lxcmgr/storage'
|
||||||
|
@ -12,7 +12,7 @@ from pkg_resources import parse_version
|
|||||||
from . import crypto
|
from . import crypto
|
||||||
from . import flock
|
from . import flock
|
||||||
from . import lxcmgr
|
from . import lxcmgr
|
||||||
from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_LOCAL_FILE, REPO_LOCK
|
from .paths import LXC_STORAGE_DIR, REPO_CACHE_DIR, REPO_CONF_FILE, REPO_LOCAL_FILE, REPO_LOCK, REPO_SIG_FILE
|
||||||
|
|
||||||
class Stage(Enum):
|
class Stage(Enum):
|
||||||
QUEUED = 1
|
QUEUED = 1
|
||||||
@ -31,7 +31,7 @@ class RepoFileNotFound(Exception):
|
|||||||
class RepoBadRequest(Exception):
|
class RepoBadRequest(Exception):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
class AppInstall:
|
class App:
|
||||||
def __init__(self, name):
|
def __init__(self, name):
|
||||||
self.name = name
|
self.name = name
|
||||||
self.stage = Stage.QUEUED
|
self.stage = Stage.QUEUED
|
||||||
@ -44,22 +44,30 @@ class AppInstall:
|
|||||||
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
|
return min(99, round(self.bytes_downloaded / self.bytes_total * 100))
|
||||||
|
|
||||||
class PkgMgr:
|
class PkgMgr:
|
||||||
def __init__(self, repo_url, repo_auth=None):
|
def __init__(self):
|
||||||
self.repo_url = repo_url
|
self.repo_url = None
|
||||||
self.repo_auth = repo_auth
|
self.repo_auth = None
|
||||||
self.installed_packages = None
|
|
||||||
self.online_packages = None
|
self.online_packages = None
|
||||||
|
|
||||||
def load_installed_packages(self):
|
|
||||||
with open(REPO_LOCAL_FILE, 'r') as f:
|
with open(REPO_LOCAL_FILE, 'r') as f:
|
||||||
self.installed_packages = json.load(f)
|
self.installed_packages = json.load(f)
|
||||||
|
|
||||||
def save_installed_packages(self, packages):
|
def save_installed_packages(self):
|
||||||
with open(REPO_LOCAL_FILE, 'w') as f:
|
with open(REPO_LOCAL_FILE, 'w') as f:
|
||||||
json.dump(packages, f, sort_keys=True, indent=4)
|
json.dump(self.installed_packages, f, sort_keys=True, indent=4)
|
||||||
|
|
||||||
|
def load_repo_conf(self):
|
||||||
|
with open(REPO_CONF_FILE, 'r') as f:
|
||||||
|
conf = json.load(f)
|
||||||
|
self.repo_url = conf['url']
|
||||||
|
user = conf['user'] if 'user' in conf and conf['user'] else None
|
||||||
|
pwd = conf['pwd'] if 'pwd' in conf and conf['pwd'] else None
|
||||||
|
self.repo_auth = (user, pwd) if user else None
|
||||||
|
|
||||||
def get_repo_resource(self, resource_url, stream=False):
|
def get_repo_resource(self, resource_url, stream=False):
|
||||||
# Download requested repository resource
|
# Download requested repository resource
|
||||||
|
if not self.repo_url:
|
||||||
|
self.load_repo_conf()
|
||||||
|
# Make a HTTP request
|
||||||
r = requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream)
|
r = requests.get('{}/{}'.format(self.repo_url, resource_url), auth=self.repo_auth, timeout=5, stream=stream)
|
||||||
if r.status_code == 401:
|
if r.status_code == 401:
|
||||||
raise RepoUnauthorized(r.url)
|
raise RepoUnauthorized(r.url)
|
||||||
@ -73,65 +81,65 @@ class PkgMgr:
|
|||||||
# Fetches and verifies online packages. Can raise InvalidSignature
|
# Fetches and verifies online packages. Can raise InvalidSignature
|
||||||
packages = self.get_repo_resource('packages').content
|
packages = self.get_repo_resource('packages').content
|
||||||
packages_sig = self.get_repo_resource('packages.sig').content
|
packages_sig = self.get_repo_resource('packages.sig').content
|
||||||
crypto.verify_signature(packages, packages_sig)
|
crypto.verify_signature(REPO_SIG_FILE, packages, packages_sig)
|
||||||
self.online_packages = json.loads(packages)
|
self.online_packages = json.loads(packages)
|
||||||
|
|
||||||
@flock.flock_ex(REPO_LOCK)
|
@flock.flock_ex(REPO_LOCK)
|
||||||
def install_app(self, app):
|
def install_app(self, app):
|
||||||
# Main installation function. Wrapper for download, registration and install script
|
# Main installation function. Wrapper for download, registration and install script
|
||||||
if not self.installed_packages:
|
|
||||||
self.load_installed_packages()
|
|
||||||
# Request for installation of already installed app immediately returns with success
|
|
||||||
if app.name in self.installed_packages['apps']:
|
if app.name in self.installed_packages['apps']:
|
||||||
app.stage = Stage.DONE
|
app.stage = Stage.DONE
|
||||||
return
|
return
|
||||||
if not self.online_packages:
|
if not self.online_packages:
|
||||||
self.fetch_online_packages()
|
self.fetch_online_packages()
|
||||||
# Get all packages on which the app depends and which have not been installed yet
|
# Get all packages on which the app depends and which have not been installed yet
|
||||||
|
#TODO: flatten and change name to "images"
|
||||||
layers = []
|
layers = []
|
||||||
images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()]
|
images = [container['image'] for container in self.online_packages['apps'][app]['containers'].values()]
|
||||||
for image in images:
|
for image in images:
|
||||||
layers.extend(self.online_packages['images'][image]['layers'])
|
layers.extend(self.online_packages['images'][image]['layers'])
|
||||||
layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']]
|
layers = [layer for layer in set(layers) if layer not in self.installed_packages['images']]
|
||||||
# Calculate bytes to download
|
# Calculate bytes to download
|
||||||
app.bytes_total = sum(self.online_packages['images'][layer]['size'] for layer in layers) + self.online_packages['apps'][app.name]['size']
|
app.bytes_total = sum(self.online_packages['images'][image]['size'] for image in layers) + self.online_packages['apps'][app.name]['size']
|
||||||
# Download layers and setup script files
|
# Download layers and setup script files
|
||||||
app.stage = Stage.DOWNLOAD
|
app.stage = Stage.DOWNLOAD
|
||||||
for layer in layers:
|
for image in layers:
|
||||||
self.download_layer(app, layer)
|
self.download_image(app, image)
|
||||||
self.download_scripts(app)
|
self.download_scripts(app)
|
||||||
# Purge old data (to clean previous failed installation) and unpack
|
# Purge old data to clean previous failed installation and unpack downloaded archives
|
||||||
app.stage = Stage.UNPACK
|
app.stage = Stage.UNPACK
|
||||||
for layer in layers:
|
for image in layers:
|
||||||
self.purge_layer(layer)
|
self.purge_image(image)
|
||||||
self.unpack_layer(layer)
|
self.unpack_image(image)
|
||||||
|
self.register_image(image, self.online_packages['images'][image])
|
||||||
self.purge_scripts(app.name)
|
self.purge_scripts(app.name)
|
||||||
self.unpack_scripts(app.name)
|
self.unpack_scripts(app.name)
|
||||||
# Run setup scripts
|
# Run setup scripts
|
||||||
app.stage = Stage.INSTALL
|
app.stage = Stage.INSTALL
|
||||||
|
# Run uninstall script to clean previous failed installation
|
||||||
self.run_uninstall_script(app.name)
|
self.run_uninstall_script(app.name)
|
||||||
# Build containers and services
|
# Build containers and services
|
||||||
self.create_containers(app.name)
|
self.create_containers(app.name)
|
||||||
# Run install script and finish the installation
|
# TODO: Create services
|
||||||
|
# Run install script and register the app
|
||||||
self.run_install_script(app.name)
|
self.run_install_script(app.name)
|
||||||
self.installed_packages['apps'][app.name] = self.online_packages['apps'][app.name]
|
self.register_app(app.name, self.online_packages['apps'][app.name])
|
||||||
self.save_installed_packages()
|
|
||||||
app.stage = Stage.DONE
|
app.stage = Stage.DONE
|
||||||
|
|
||||||
def download_layer(self, app, layer):
|
def download_image(self, app, image):
|
||||||
pkg_archive = 'images/{}.tar.xz'.format(layer)
|
# Download image archive and verify hash
|
||||||
|
pkg_archive = 'images/{}.tar.xz'.format(image)
|
||||||
self.download_archive(app, pkg_archive)
|
self.download_archive(app, pkg_archive)
|
||||||
# Verify hash
|
crypto.verify_hash(tmp_archive, self.online_packages['images'][image]['sha512'])
|
||||||
crypto.verify_hash(tmp_archive, self.online_packages['images'][layer]['sha512'])
|
|
||||||
|
|
||||||
def download_scripts(self, app):
|
def download_scripts(self, app):
|
||||||
|
# Download scripts archive and verify hash
|
||||||
pkg_archive = 'apps/{}.tar.xz'.format(app.name)
|
pkg_archive = 'apps/{}.tar.xz'.format(app.name)
|
||||||
self.download_archive(app, pkg_archive)
|
self.download_archive(app, pkg_archive)
|
||||||
# Verify hash
|
|
||||||
crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512'])
|
crypto.verify_hash(tmp_archive, self.online_packages['apps'][app.name]['sha512'])
|
||||||
|
|
||||||
def download_archive(self, app, archive):
|
def download_archive(self, app, archive):
|
||||||
# Download the archive
|
# Download the archive from online repository
|
||||||
tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive)
|
tmp_archive = os.path.join(REPO_CACHE_DIR, pkg_archive)
|
||||||
res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True)
|
res = self.get_repo_resource('{}/{}'.format(type, pkg_archive), True)
|
||||||
with open(tmp_archive, 'wb') as f:
|
with open(tmp_archive, 'wb') as f:
|
||||||
@ -139,26 +147,30 @@ class PkgMgr:
|
|||||||
if chunk:
|
if chunk:
|
||||||
app.bytes_downloaded += f.write(chunk)
|
app.bytes_downloaded += f.write(chunk)
|
||||||
|
|
||||||
def purge_layer(self, layer):
|
def purge_image(self, image):
|
||||||
# Delete layer files from storage directory
|
# Delete layer files from storage directory
|
||||||
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, layer))
|
shutil.rmtree(os.path.join(LXC_STORAGE_DIR, image))
|
||||||
if layer in self.installed_packages['images']:
|
|
||||||
del self.installed_packages['images'][layer]
|
|
||||||
self.save_installed_packages()
|
|
||||||
|
|
||||||
def unpack_layer(self, layer):
|
def unpack_image(self, image):
|
||||||
# Unpack layer archive
|
# Unpack layer archive
|
||||||
tmp_archive = os.path.join(REPO_CACHE_DIR, 'images/{}.tar.xz'.format(layer))
|
tmp_archive = os.path.join(REPO_CACHE_DIR, 'images/{}.tar.xz'.format(image))
|
||||||
subprocess.run(['tar', 'xJf', tmp_archive], cwd=LXC_STORAGE_DIR, check=True)
|
subprocess.run(['tar', 'xJf', tmp_archive], cwd=LXC_STORAGE_DIR, check=True)
|
||||||
os.unlink(tmp_archive)
|
os.unlink(tmp_archive)
|
||||||
self.installed_packages['images'][layer] = self.online_packages['images'][layer]
|
|
||||||
|
def register_image(self, image, metadata):
|
||||||
|
# Add installed layer to list of installed images
|
||||||
|
self.installed_packages['images'][image] = metadata
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
|
def unregister_image(self, image):
|
||||||
|
# Remove image from list of installed images
|
||||||
|
if image in self.installed_packages['images']:
|
||||||
|
del self.installed_packages['images'][image]
|
||||||
self.save_installed_packages()
|
self.save_installed_packages()
|
||||||
|
|
||||||
def purge_scripts(self, app):
|
def purge_scripts(self, app):
|
||||||
# Delete application setup scripts from storage directory
|
# Delete application setup scripts from storage directory
|
||||||
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app))
|
shutil.rmtree(os.path.join(REPO_CACHE_DIR, 'apps', app))
|
||||||
del self.installed_packages['apps'][app]
|
|
||||||
self.save_installed_packages()
|
|
||||||
|
|
||||||
def unpack_scripts(self, app):
|
def unpack_scripts(self, app):
|
||||||
# Unpack setup scripts archive
|
# Unpack setup scripts archive
|
||||||
@ -180,6 +192,17 @@ class PkgMgr:
|
|||||||
if os.path.exists(script_path):
|
if os.path.exists(script_path):
|
||||||
subprocess.run(script_path, check=True)
|
subprocess.run(script_path, check=True)
|
||||||
|
|
||||||
|
def register_app(self, app, metadata):
|
||||||
|
# Register installed app in list of installed apps
|
||||||
|
self.installed_packages['apps'][app] = metadata
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
|
def unregister_app(self, app):
|
||||||
|
# Remove app from list of installed apps
|
||||||
|
if app in self.installed_packages['apps']:
|
||||||
|
del self.installed_packages['apps'][app]
|
||||||
|
self.save_installed_packages()
|
||||||
|
|
||||||
def create_containers(self, app):
|
def create_containers(self, app):
|
||||||
# Create LXC containers from image and app metadata
|
# Create LXC containers from image and app metadata
|
||||||
for container in self.online_packages['apps'][app]['containers']:
|
for container in self.online_packages['apps'][app]['containers']:
|
||||||
@ -188,10 +211,13 @@ class PkgMgr:
|
|||||||
if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
|
if 'mounts' in self.online_packages['apps'][app]['containers'][container]:
|
||||||
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
|
image['mounts'] = self.online_packages['apps'][app]['containers'][container]['mounts']
|
||||||
lxcmgr.create_container(container, image)
|
lxcmgr.create_container(container, image)
|
||||||
|
# TODO: Create services
|
||||||
|
|
||||||
@flock.flock_ex(REPO_LOCK)
|
@flock.flock_ex(REPO_LOCK)
|
||||||
def uninstall_app(self, app):
|
def uninstall_app(self, app):
|
||||||
# Main uninstallation function. Wrapper for uninstall script and filesystem purge
|
# Main uninstallation function. Wrapper for uninstall script and filesystem purge
|
||||||
|
if app not in self.installed_packages['apps']:
|
||||||
|
return
|
||||||
self.run_uninstall_script(app)
|
self.run_uninstall_script(app)
|
||||||
self.destroy_containers(app)
|
self.destroy_containers(app)
|
||||||
self.purge_scripts(app)
|
self.purge_scripts(app)
|
||||||
@ -218,8 +244,8 @@ class PkgMgr:
|
|||||||
def update_app(self, app, item):
|
def update_app(self, app, item):
|
||||||
# Main update function.
|
# Main update function.
|
||||||
# TODO: Implement actual update
|
# TODO: Implement actual update
|
||||||
#uninstall_app(app)
|
uninstall_app(app)
|
||||||
#install_app(app, item)
|
install_app(app, item)
|
||||||
|
|
||||||
def has_update(self, app):
|
def has_update(self, app):
|
||||||
if not self.installed_packages:
|
if not self.installed_packages:
|
||||||
|
@ -40,8 +40,8 @@ lxc.idmap = u 0 100000 65536
|
|||||||
lxc.idmap = g 0 100000 65536
|
lxc.idmap = g 0 100000 65536
|
||||||
|
|
||||||
# Hooks
|
# Hooks
|
||||||
lxc.hook.pre-start = /usr/bin/vmmgr prepare-container {layers}
|
lxc.hook.pre-start = /usr/bin/lxcmgr container prepare {layers}
|
||||||
lxc.hook.post-stop = /usr/bin/vmmgr cleanup-container
|
lxc.hook.post-stop = /usr/bin/lxcmgr container cleanup
|
||||||
|
|
||||||
# Other
|
# Other
|
||||||
lxc.arch = linux64
|
lxc.arch = linux64
|
||||||
|
1
var/lib/lxcmgr/packages
Normal file
1
var/lib/lxcmgr/packages
Normal file
@ -0,0 +1 @@
|
|||||||
|
{"apps": {}, "images": {}}
|
Loading…
Reference in New Issue
Block a user