2178 lines
74 KiB
Python
2178 lines
74 KiB
Python
# Copyright (c) 2014-2019, iocage
|
|
# All rights reserved.
|
|
#
|
|
# Redistribution and use in source and binary forms, with or without
|
|
# modification, are permitted providing that the following conditions
|
|
# are met:
|
|
# 1. Redistributions of source code must retain the above copyright
|
|
# notice, this list of conditions and the following disclaimer.
|
|
# 2. Redistributions in binary form must reproduce the above copyright
|
|
# notice, this list of conditions and the following disclaimer in the
|
|
# documentation and/or other materials provided with the distribution.
|
|
#
|
|
# THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR
|
|
# IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
# WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE
|
|
# ARE DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY
|
|
# DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
|
|
# DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS
|
|
# OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION)
|
|
# HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT,
|
|
# STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING
|
|
# IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE
|
|
# POSSIBILITY OF SUCH DAMAGE.
|
|
|
|
import collections
|
|
import datetime
|
|
import json
|
|
import operator
|
|
import os
|
|
import subprocess as su
|
|
|
|
import iocage_lib.ioc_clean as ioc_clean
|
|
import iocage_lib.ioc_common as ioc_common
|
|
import iocage_lib.ioc_create as ioc_create
|
|
import iocage_lib.ioc_destroy as ioc_destroy
|
|
import iocage_lib.ioc_exec as ioc_exec
|
|
import iocage_lib.ioc_fetch as ioc_fetch
|
|
import iocage_lib.ioc_fstab as ioc_fstab
|
|
import iocage_lib.ioc_image as ioc_image
|
|
import iocage_lib.ioc_json as ioc_json
|
|
import iocage_lib.ioc_list as ioc_list
|
|
import iocage_lib.ioc_plugin as ioc_plugin
|
|
import iocage_lib.ioc_start as ioc_start
|
|
import iocage_lib.ioc_stop as ioc_stop
|
|
import iocage_lib.ioc_upgrade as ioc_upgrade
|
|
import iocage_lib.ioc_debug as ioc_debug
|
|
import iocage_lib.ioc_exceptions as ioc_exceptions
|
|
|
|
from iocage_lib.cache import cache
|
|
from iocage_lib.dataset import Dataset
|
|
from iocage_lib.pools import Pool, PoolListableResource
|
|
from iocage_lib.release import Release
|
|
from iocage_lib.snapshot import SnapshotListableResource, Snapshot
|
|
|
|
|
|
class PoolAndDataset:
|
|
|
|
def get_pool(self):
|
|
"""
|
|
Helper to get the current pool.
|
|
|
|
Return:
|
|
string: with the pool name.
|
|
"""
|
|
|
|
return ioc_json.IOCJson().json_get_value("pool")
|
|
|
|
def get_iocroot(self):
|
|
"""
|
|
Helper to get the iocroot.
|
|
|
|
Return:
|
|
string: with the iocroot name.
|
|
"""
|
|
return ioc_json.IOCJson().json_get_value("iocroot")
|
|
|
|
|
|
class IOCage:
|
|
|
|
def __init__(
|
|
self, jail=None, rc=False, callback=None, silent=False,
|
|
activate=False, skip_jails=False, reset_cache=False,
|
|
):
|
|
self.rc = rc
|
|
self.silent = silent
|
|
|
|
# FreeNAS won't be entering through the CLI, so we set sane defaults
|
|
os.environ.get("IOCAGE_SKIP", "FALSE")
|
|
os.environ.get("IOCAGE_FORCE", "TRUE")
|
|
|
|
if reset_cache:
|
|
self.reset_cache()
|
|
|
|
if not activate:
|
|
self.generic_iocjson = ioc_json.IOCJson()
|
|
self.pool = self.generic_iocjson.pool
|
|
self.iocroot = self.generic_iocjson.iocroot
|
|
|
|
if not skip_jails:
|
|
# When they need to destroy a jail with a missing or bad
|
|
# configuration, this gets in our way otherwise.
|
|
self.jails = self.list("uuid")
|
|
|
|
self.skip_jails = skip_jails
|
|
self.jail = jail
|
|
self._all = True if self.jail and 'ALL' in self.jail else False
|
|
self.callback = callback
|
|
self.is_depend = False
|
|
|
|
@staticmethod
|
|
def reset_cache():
|
|
cache.reset()
|
|
|
|
def __all__(self, jail_order, action, ignore_exception=False):
|
|
# So we can properly start these.
|
|
self._all = False
|
|
|
|
for j in jail_order:
|
|
# We want this to be the real jail now.
|
|
self.jail = j
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
|
|
if action == 'stop':
|
|
self.stop(j, ignore_exception=ignore_exception)
|
|
elif action == 'start':
|
|
if not status:
|
|
err, msg = self.start(j, ignore_exception=ignore_exception)
|
|
|
|
if err:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'ERROR',
|
|
'message': msg
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
else:
|
|
message = f"{uuid} ({j}) is already running!"
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'WARNING',
|
|
'message': message
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
|
|
def __jail_order__(self, action, ignore_exception=False):
|
|
"""Helper to gather lists of all the jails by order and boot order."""
|
|
jail_order = {}
|
|
boot_order = {}
|
|
|
|
_reverse = True if action == 'stop' else False
|
|
|
|
for jail in self.jails:
|
|
self.jail = jail
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(path).json_get_value('all')
|
|
boot = conf['boot']
|
|
priority = conf['priority']
|
|
jail_order[jail] = int(priority)
|
|
|
|
# This removes having to grab all the JSON again later.
|
|
|
|
if boot:
|
|
boot_order[jail] = int(priority)
|
|
|
|
jail_order = collections.OrderedDict(
|
|
sorted(
|
|
jail_order.items(),
|
|
key=operator.itemgetter(1),
|
|
reverse=_reverse))
|
|
boot_order = collections.OrderedDict(
|
|
sorted(
|
|
boot_order.items(),
|
|
key=operator.itemgetter(1),
|
|
reverse=_reverse))
|
|
|
|
if self.rc:
|
|
self.__rc__(boot_order, action, ignore_exception)
|
|
elif self._all:
|
|
self.__all__(jail_order, action, ignore_exception)
|
|
|
|
def __rc__(self, boot_order, action, ignore_exception=False):
|
|
"""Helper to start all jails with boot=on"""
|
|
# So we can properly start these.
|
|
self.rc = False
|
|
|
|
for j in boot_order.keys():
|
|
# We want this to be the real jail now.
|
|
self.jail = j
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, _ = self.list("jid", uuid=uuid)
|
|
|
|
if action == 'stop':
|
|
if status:
|
|
message = f" Stopping {uuid}"
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': message
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
|
|
self.stop(j, ignore_exception=ignore_exception)
|
|
else:
|
|
message = f"{uuid} is not running!"
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': message
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
elif action == 'start':
|
|
if not status:
|
|
message = f" Starting {uuid}"
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': message
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
|
|
err, msg = self.start(j, ignore_exception=ignore_exception)
|
|
|
|
if err:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'ERROR',
|
|
'message': msg
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
else:
|
|
message = f"{uuid} is already running!"
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'WARNING',
|
|
'message': message
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
|
|
def __check_jail_existence__(self):
|
|
"""
|
|
Helper to check if jail dataset exists
|
|
Return:
|
|
tuple: The jails uuid, path
|
|
"""
|
|
|
|
if os.path.isdir(f"{self.iocroot}/jails/{self.jail}"):
|
|
path = f"{self.iocroot}/jails/{self.jail}"
|
|
|
|
return self.jail, path
|
|
elif os.path.isdir(f"{self.iocroot}/templates/{self.jail}"):
|
|
path = f"{self.iocroot}/templates/{self.jail}"
|
|
|
|
return self.jail, path
|
|
else:
|
|
if self.skip_jails:
|
|
# We skip jails for performance, but if they didn't match be
|
|
# now need to gather the list and iterate.
|
|
self.jails = self.list("uuid")
|
|
|
|
# We got a partial, time to search.
|
|
_jail = {
|
|
uuid: path
|
|
|
|
for (uuid, path) in self.jails.items()
|
|
|
|
if uuid.startswith(self.jail)
|
|
}
|
|
|
|
if len(_jail) == 1:
|
|
uuid, path = next(iter(_jail.items()))
|
|
|
|
return uuid, path
|
|
elif len(_jail) > 1:
|
|
msg = f"Multiple jails found for {self.jail}:"
|
|
|
|
for u, p in sorted(_jail.items()):
|
|
msg += f"\n {u} ({p})"
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": msg
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
msg = f"jail '{self.jail}' not found!"
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": msg
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
@staticmethod
|
|
def __check_jail_type__(_type, uuid):
|
|
"""
|
|
Return:
|
|
tuple: True if error with a message, or False/None
|
|
"""
|
|
|
|
if _type in ("jail", "plugin", "clonejail", "pluginv2"):
|
|
return False, None
|
|
elif _type == 'basejail':
|
|
return (True, "Please run \"iocage migrate\" before trying to"
|
|
f" start {uuid}")
|
|
elif _type == 'template':
|
|
return (True, "Please convert back to a jail before trying to"
|
|
f" start {uuid}")
|
|
else:
|
|
return True, f"{_type} is not a supported jail type."
|
|
|
|
@staticmethod
|
|
def __mount__(path, _type):
|
|
if _type == "devfs":
|
|
cmd = ["mount", "-t", "devfs", "devfs", path]
|
|
else:
|
|
cmd = ["mount", "-a", "-F", path]
|
|
|
|
_, stderr = su.Popen(cmd, stdout=su.PIPE, stderr=su.PIPE).communicate()
|
|
|
|
return stderr
|
|
|
|
@staticmethod
|
|
def __umount__(path, _type):
|
|
if _type == "devfs":
|
|
cmd = ["umount", path]
|
|
else:
|
|
cmd = ["umount", "-a", "-F", path]
|
|
|
|
_, stderr = su.Popen(cmd, stdout=su.PIPE, stderr=su.PIPE).communicate()
|
|
|
|
return stderr
|
|
|
|
def activate(self, zpool):
|
|
"""Activates the zpool for iocage usage"""
|
|
zpool = Pool(zpool, cache=False)
|
|
if not zpool.exists:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"ZFS pool '{zpool}' not found!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
for pool in PoolListableResource():
|
|
if pool == zpool:
|
|
locked_error = None
|
|
if pool.root_dataset.locked:
|
|
locked_error = f'ZFS pool "{zpool}" root dataset is locked'
|
|
|
|
iocage_ds = Dataset(os.path.join(zpool.name, 'iocage'))
|
|
if iocage_ds.exists and iocage_ds.locked:
|
|
locked_error = f'ZFS dataset "{iocage_ds.name}" is locked'
|
|
if locked_error:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION',
|
|
'message': locked_error,
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent,
|
|
)
|
|
else:
|
|
pool.activate_pool()
|
|
else:
|
|
pool.deactivate_pool()
|
|
|
|
def deactivate(self, zpool):
|
|
zpool = Pool(zpool, cache=False)
|
|
if not zpool.exists:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION',
|
|
'message': f'ZFS pool "{zpool}" not found!'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
zpool.deactivate_pool()
|
|
|
|
def chroot(self, command):
|
|
"""Deprecated: Chroots into a jail and runs a command, or the shell."""
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message":
|
|
(
|
|
"iocage chroot is deprecated. "
|
|
"If you need to execute a {} inside the jail use: {}"
|
|
).format(*[
|
|
["shell", "iocage console"],
|
|
["command", "iocage exec"]
|
|
][int(len(command) != 0)])
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def clean(self, d_type):
|
|
"""Destroys all of a specified dataset types."""
|
|
if d_type == 'jails':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_jails()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': 'All iocage jail datasets have been destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif d_type == 'all':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_all()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': 'All iocage datasets have been destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif d_type == 'release':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_releases()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': 'All iocage RELEASE and jail datasets have been'
|
|
' destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif d_type == 'template':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_templates()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message':
|
|
'All iocage template datasets have been destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif d_type == 'images':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_images()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': 'The iocage images dataset has been destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif d_type == 'debug':
|
|
ioc_clean.IOCClean(silent=self.silent).clean_debug()
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': 'All iocage debugs have been destroyed.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": "Please specify a dataset type to clean!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def create(self,
|
|
release,
|
|
props,
|
|
count=0,
|
|
pkglist=None,
|
|
template=False,
|
|
short=False,
|
|
_uuid=None,
|
|
basejail=False,
|
|
thickjail=False,
|
|
empty=False,
|
|
clone=None,
|
|
skip_batch=False,
|
|
thickconfig=False,
|
|
clone_basejail=False):
|
|
"""Creates the jail dataset"""
|
|
count = 0 if count == 1 and not skip_batch else count
|
|
|
|
if short and _uuid:
|
|
_uuid = _uuid[:8]
|
|
|
|
if len(_uuid) != 8:
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Need a minimum of 8 characters to use --short"
|
|
" (-s) and --uuid (-u) together!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if not template and not release and not empty and not clone:
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Must supply either --template (-t) or"
|
|
" --release (-r)!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if release is not None:
|
|
if os.path.isdir(
|
|
f'{self.iocroot}/releases/{release.upper()}'
|
|
) and not template and not empty and not clone:
|
|
release = release.upper()
|
|
|
|
if not os.path.isdir(
|
|
f'{self.iocroot}/releases/{release}'
|
|
) and not template and not empty and not clone:
|
|
freebsd_version = ioc_common.checkoutput(["freebsd-version"])
|
|
|
|
if "HBSD" in freebsd_version:
|
|
hardened = True
|
|
else:
|
|
hardened = False
|
|
|
|
arch = os.uname()[4]
|
|
|
|
if arch in {'i386', 'arm64'}:
|
|
files = ['MANIFEST', 'base.txz', 'src.txz']
|
|
else:
|
|
files = ['MANIFEST', 'base.txz', 'lib32.txz', 'src.txz']
|
|
|
|
try:
|
|
if int(release.rsplit('-')[0].rsplit('.')[0]) < 12:
|
|
# doc.txz has relevance here still
|
|
files.append('doc.txz')
|
|
except (AttributeError, ValueError):
|
|
# Non-standard naming scheme, assuming it's current
|
|
pass
|
|
|
|
ioc_fetch.IOCFetch(
|
|
release,
|
|
hardened=hardened,
|
|
files=files,
|
|
silent=self.silent
|
|
).fetch_release()
|
|
|
|
if clone:
|
|
clone_uuid, path = self.__check_jail_existence__()
|
|
|
|
if 'templates' in path:
|
|
template = True
|
|
|
|
status, _ = self.list("jid", uuid=clone_uuid)
|
|
|
|
if status:
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
f"Jail: {self.jail} must not be running to be"
|
|
" cloned!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
release = clone_uuid
|
|
clone = self.jail
|
|
|
|
try:
|
|
if count > 1 and not skip_batch:
|
|
for j in range(1, count + 1):
|
|
|
|
self.create(
|
|
release,
|
|
props,
|
|
j,
|
|
pkglist=pkglist,
|
|
template=template,
|
|
short=short,
|
|
_uuid=f"{_uuid}_{j}" if _uuid else None,
|
|
basejail=basejail,
|
|
thickjail=thickjail,
|
|
empty=empty,
|
|
clone=clone,
|
|
skip_batch=True,
|
|
thickconfig=thickconfig,
|
|
clone_basejail=clone_basejail)
|
|
else:
|
|
ioc_create.IOCCreate(
|
|
release,
|
|
props,
|
|
count,
|
|
pkglist,
|
|
silent=self.silent,
|
|
template=template,
|
|
short=short,
|
|
basejail=basejail,
|
|
thickjail=thickjail,
|
|
empty=empty,
|
|
uuid=_uuid,
|
|
clone=clone,
|
|
thickconfig=thickconfig,
|
|
clone_basejail=clone_basejail
|
|
).create_jail()
|
|
except BaseException:
|
|
if clone:
|
|
su.run(
|
|
[
|
|
'zfs', 'destroy', '-r',
|
|
f'{self.pool}/iocage/jails/{clone}@{_uuid}'
|
|
]
|
|
)
|
|
raise
|
|
|
|
return False, None
|
|
|
|
def destroy_release(self, download=False):
|
|
"""Destroy supplied RELEASE and the download dataset if asked"""
|
|
path = f"{self.pool}/iocage/releases/{self.jail}"
|
|
|
|
release = Release(self.jail)
|
|
# Let's make sure the release exists before we try to destroy it
|
|
if not release:
|
|
ioc_common.logit({
|
|
'level': 'EXCEPTION',
|
|
'message': f'Release: {self.jail} not found!'
|
|
})
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Destroying RELEASE dataset: {self.jail}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
ioc_destroy.IOCDestroy().__destroy_parse_datasets__(path, stop=False)
|
|
|
|
if download:
|
|
path = f"{self.pool}/iocage/download/{self.jail}"
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message":
|
|
f"Destroying RELEASE download dataset: {self.jail}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
ioc_destroy.IOCDestroy().__destroy_parse_datasets__(path,
|
|
stop=False)
|
|
|
|
def destroy_jail(self, force=False):
|
|
"""
|
|
Destroys the supplied jail, to reduce perfomance hit,
|
|
call IOCage with skip_jails=True
|
|
"""
|
|
try:
|
|
self.jails = self.list("uuid")
|
|
except (RuntimeError, SystemExit) as err:
|
|
err = str(err)
|
|
|
|
if "Configuration is missing" in err:
|
|
uuid = err.split()[5]
|
|
path = f"{self.pool}/iocage/jails/{uuid}"
|
|
|
|
if uuid == self.jail:
|
|
ioc_destroy.IOCDestroy().__destroy_parse_datasets__(
|
|
path, stop=False)
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"{uuid} destroyed"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
return
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": err
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
except FileNotFoundError as err:
|
|
# Jail is lacking a configuration, time to nuke it from orbit.
|
|
uuid = str(err).rsplit("/")[-2]
|
|
path = f"{self.pool}/iocage/jails/{uuid}"
|
|
|
|
if uuid == self.jail:
|
|
ioc_destroy.IOCDestroy().__destroy_parse_datasets__(path)
|
|
|
|
return
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": err
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, _ = self.list("jid", uuid=uuid)
|
|
|
|
if status:
|
|
if not force:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": (f"Jail {uuid} is still running, "
|
|
f"please stop the jail first "
|
|
f"or destroy it with -f")
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Stopping {uuid}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Destroying {uuid}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
ioc_destroy.IOCDestroy().destroy_jail(path)
|
|
|
|
def df(self):
|
|
"""Returns a list containing the resource usage of all jails"""
|
|
jail_list = []
|
|
|
|
for jail, path in self.jails.items():
|
|
conf = ioc_json.IOCJson(path).json_get_value('all')
|
|
mountpoint = f"{self.pool}/iocage/jails/{jail}"
|
|
|
|
template = conf["type"]
|
|
|
|
if template == "template":
|
|
mountpoint = f"{self.pool}/iocage/templates/{jail}"
|
|
|
|
ds = Dataset(mountpoint)
|
|
zconf = ds.properties
|
|
|
|
compressratio = zconf["compressratio"]
|
|
reservation = zconf["reservation"]
|
|
quota = zconf["quota"]
|
|
used = zconf["used"]
|
|
available = zconf["available"]
|
|
|
|
jail_list.append(
|
|
[jail, compressratio, reservation, quota, used, available])
|
|
|
|
return jail_list
|
|
|
|
def exec_all(
|
|
self, command, host_user='root', jail_user=None, console=False,
|
|
start_jail=False, interactive=False, unjailed=False, msg_return=False
|
|
):
|
|
"""Runs exec for all jails"""
|
|
self._all = False
|
|
for jail in self.jails:
|
|
self.jail = jail
|
|
self.exec(
|
|
command, host_user, jail_user, console, start_jail,
|
|
interactive, unjailed, msg_return
|
|
)
|
|
|
|
def exec(
|
|
self, command, host_user='root', jail_user=None, console=False,
|
|
start_jail=False, interactive=False, unjailed=False, msg_return=False
|
|
):
|
|
"""Executes a command in the jail as the supplied users."""
|
|
if self._all:
|
|
self.exec_all(
|
|
command, host_user, jail_user, console, start_jail,
|
|
interactive, unjailed, msg_return
|
|
)
|
|
return
|
|
|
|
pkg = unjailed
|
|
|
|
if host_user and jail_user is not None:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION',
|
|
'message': 'Please only specify either host_user or'
|
|
' jail_user, not both!'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
exec_clean = self.get('exec_clean')
|
|
|
|
if exec_clean:
|
|
env_path = '/sbin:/bin:/usr/sbin:/usr/bin:/usr/local/sbin:' \
|
|
'/usr/local/bin:/root/bin'
|
|
env_lang = os.environ.get('LANG', 'en_US.UTF-8')
|
|
su_env = {
|
|
'PATH': env_path,
|
|
'PWD': '/',
|
|
'HOME': '/',
|
|
'TERM': 'xterm-256color',
|
|
'LANG': env_lang,
|
|
'LC_ALL': env_lang
|
|
}
|
|
else:
|
|
su_env = os.environ.copy()
|
|
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
|
|
if not status and not start_jail:
|
|
if not ioc_common.INTERACTIVE:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f'{self.jail} is not running! Please supply'
|
|
' start_jail=True or start the jail'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f'{self.jail} is not running! Please supply'
|
|
' --force (-f) or start the jail'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
elif not status:
|
|
self.start()
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
|
|
if pkg:
|
|
ip4_addr = self.get("ip4_addr")
|
|
ip6_addr = self.get("ip6_addr")
|
|
dhcp = self.get("dhcp")
|
|
nat = self.get('nat')
|
|
|
|
if (
|
|
ip4_addr == ip6_addr == "none" and not dhcp and not nat
|
|
):
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"The jail requires an IP address before you "
|
|
"can use pkg. Set one and restart the jail."
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
command = ["pkg", "-j", jid] + list(command)
|
|
|
|
if console:
|
|
login_flags = self.get('login_flags').split()
|
|
console_cmd = ['login', '-p'] + login_flags
|
|
|
|
try:
|
|
ioc_exec.InteractiveExec(console_cmd, path, uuid=uuid)
|
|
except BaseException as e:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'ERROR',
|
|
'message': 'Console failed!\nThe cause could be bad '
|
|
f'permissions for {path}/root/usr/lib.'
|
|
},
|
|
_callback=self.callback,
|
|
silent=False
|
|
)
|
|
raise e
|
|
return
|
|
|
|
if interactive:
|
|
ioc_exec.InteractiveExec(
|
|
command,
|
|
path,
|
|
uuid=uuid,
|
|
host_user=host_user,
|
|
jail_user=jail_user,
|
|
skip=True
|
|
)
|
|
return
|
|
|
|
try:
|
|
with ioc_exec.IOCExec(
|
|
command,
|
|
path,
|
|
uuid=uuid,
|
|
host_user=host_user,
|
|
jail_user=jail_user,
|
|
unjailed=pkg,
|
|
su_env=su_env
|
|
) as _exec:
|
|
output = ioc_common.consume_and_log(
|
|
_exec
|
|
)
|
|
|
|
if msg_return:
|
|
return output['stdout']
|
|
|
|
for line in output['stdout']:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": line
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
except ioc_exceptions.CommandFailed as e:
|
|
msgs = [_msg.decode().rstrip() for _msg in e.message]
|
|
if msgs:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": '\n'.join(msgs)
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f'Command: {command} failed!'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def export(self, compression_algo='zip'):
|
|
"""Will export a jail"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, _ = self.list("jid", uuid=uuid)
|
|
|
|
if status:
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
f"{uuid} is running, stop the jail before"
|
|
" exporting!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
ioc_image.IOCImage().export_jail(
|
|
uuid, path, compression_algo=compression_algo
|
|
)
|
|
|
|
def fetch(self, **kwargs):
|
|
"""Fetches a release or plugin."""
|
|
release = kwargs.pop("release", None)
|
|
name = kwargs.pop("name", None)
|
|
props = kwargs.pop("props", ())
|
|
plugins = kwargs.pop("plugins", False)
|
|
plugin_name = kwargs.pop("plugin_name", None)
|
|
count = kwargs.pop("count", 1)
|
|
accept = kwargs.pop("accept", False)
|
|
_list = kwargs.pop("list", False)
|
|
remote = kwargs.pop("remote", False)
|
|
http = kwargs.get("http", True)
|
|
hardened = kwargs.get("hardened", False)
|
|
header = kwargs.pop("header", True)
|
|
_long = kwargs.pop("_long", False)
|
|
official = kwargs.pop("official", False)
|
|
branch = kwargs.pop("branch", None)
|
|
keep_jail_on_failure = kwargs.pop("keep_jail_on_failure", False)
|
|
thick_config = kwargs.pop("thickconfig", False)
|
|
|
|
freebsd_version = ioc_common.checkoutput(["freebsd-version"])
|
|
arch = os.uname()[4]
|
|
|
|
if not _list:
|
|
if not kwargs.get('files', None):
|
|
if arch in {'i386', 'arm64'}:
|
|
kwargs['files'] = ['MANIFEST', 'base.txz', 'src.txz']
|
|
else:
|
|
kwargs['files'] = ['MANIFEST', 'base.txz', 'lib32.txz',
|
|
'src.txz']
|
|
|
|
try:
|
|
if int(release.rsplit('-')[0].rsplit('.')[0]) < 12:
|
|
# doc.txz has relevance here still
|
|
kwargs['files'].append('doc.txz')
|
|
except (AttributeError, ValueError):
|
|
# Non-standard naming scheme, assuming it's current
|
|
pass
|
|
|
|
if "HBSD" in freebsd_version:
|
|
if kwargs["server"] == "download.freebsd.org":
|
|
kwargs["hardened"] = True
|
|
else:
|
|
kwargs["hardened"] = False
|
|
else:
|
|
kwargs["hardened"] = False
|
|
|
|
if plugins or plugin_name:
|
|
if _list:
|
|
rel_list = ioc_plugin.IOCPlugin(
|
|
branch=branch,
|
|
thickconfig=thick_config,
|
|
**kwargs
|
|
).fetch_plugin_index(
|
|
"", _list=True, list_header=header, list_long=_long,
|
|
icon=True, official=official
|
|
)
|
|
|
|
return rel_list
|
|
|
|
if plugins:
|
|
ioc_plugin.IOCPlugin(
|
|
release=release,
|
|
plugin=plugin_name,
|
|
branch=branch,
|
|
thickconfig=thick_config,
|
|
**kwargs).fetch_plugin_index(
|
|
props, accept_license=accept, official=official)
|
|
|
|
return
|
|
|
|
plugin_obj = ioc_plugin.IOCPlugin(
|
|
release=release, plugin=plugin_name,
|
|
branch=branch, silent=self.silent,
|
|
keep_jail_on_failure=keep_jail_on_failure,
|
|
callback=self.callback, **kwargs,
|
|
thickconfig=thick_config,
|
|
)
|
|
|
|
i = 1
|
|
check_jail_name = name or plugin_obj.retrieve_plugin_json().get(
|
|
'name', plugin_name
|
|
)
|
|
while True:
|
|
if check_jail_name not in self.jails:
|
|
jail_name = check_jail_name
|
|
break
|
|
elif f'{check_jail_name}_{i}' not in self.jails:
|
|
jail_name = f'{check_jail_name}_{i}'
|
|
break
|
|
i += 1
|
|
|
|
self.jails[jail_name] = jail_name # Not a valid value
|
|
if count == 1:
|
|
plugin_obj.jail = jail_name
|
|
plugin_obj.fetch_plugin(props, 0, accept)
|
|
else:
|
|
for j in range(1, count + 1):
|
|
# Repeating this block in case they have gaps in their
|
|
# plugins
|
|
# Allows plugin_1, plugin_2, and such to happen instead of
|
|
# plugin_1_1, plugin_1_2
|
|
while True:
|
|
if jail_name not in self.jails:
|
|
break
|
|
elif f'{check_jail_name}_{i}' not in self.jails:
|
|
jail_name = f'{check_jail_name}_{i}'
|
|
break
|
|
|
|
i += 1
|
|
|
|
self.jails[jail_name] = jail_name # Not a valid value
|
|
plugin_obj.jail = jail_name
|
|
plugin_obj.fetch_plugin(props, j, accept)
|
|
else:
|
|
kwargs.pop('git_repository', None)
|
|
kwargs.pop('git_destination', None)
|
|
|
|
if _list:
|
|
if remote:
|
|
rel_list = ioc_fetch.IOCFetch(
|
|
"", http=http, hardened=hardened).fetch_release(
|
|
_list=True)
|
|
else:
|
|
rel_list = self.list("base")
|
|
|
|
return rel_list
|
|
|
|
ioc_fetch.IOCFetch(
|
|
release,
|
|
silent=self.silent, callback=self.callback,
|
|
**kwargs).fetch_release()
|
|
|
|
def fstab(self,
|
|
action,
|
|
source,
|
|
destination,
|
|
fstype,
|
|
options,
|
|
dump,
|
|
_pass,
|
|
index=None,
|
|
add_path=False,
|
|
header=False):
|
|
"""Adds an fstab entry for a jail"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
|
|
if action != "list":
|
|
if add_path:
|
|
destination = f"{self.iocroot}/jails/{uuid}/root{destination}"
|
|
|
|
if destination and len(destination) > 88:
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"WARNING",
|
|
"message":
|
|
"The destination's mountpoint exceeds 88 "
|
|
"characters, this may cause failure!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if action == "list":
|
|
fstab = ioc_fstab.IOCFstab(
|
|
uuid,
|
|
action,
|
|
source,
|
|
destination,
|
|
fstype,
|
|
options,
|
|
dump,
|
|
_pass,
|
|
index=index,
|
|
header=header,
|
|
).fstab_list()
|
|
|
|
return fstab
|
|
else:
|
|
ioc_fstab.IOCFstab(
|
|
uuid,
|
|
action,
|
|
source,
|
|
destination,
|
|
fstype,
|
|
options,
|
|
dump,
|
|
_pass,
|
|
index=index
|
|
)
|
|
|
|
def get(
|
|
self, prop, recursive=False, plugin=False, pool=False, start_jail=False
|
|
):
|
|
"""Get a jail property"""
|
|
if start_jail and not plugin:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION',
|
|
'message':
|
|
'--force (-f) is only applicable with --plugin (-P)!'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if pool:
|
|
return self.pool
|
|
|
|
if not recursive:
|
|
if self.jail == "default":
|
|
try:
|
|
return ioc_json.IOCJson().json_get_value(prop,
|
|
default=True)
|
|
except KeyError:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{prop} is not a valid property!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
|
|
state = "up" if status else "down"
|
|
|
|
if prop == "state":
|
|
return state
|
|
elif plugin:
|
|
if not status and not start_jail:
|
|
if not ioc_common.INTERACTIVE:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f'{self.jail} is not running!'
|
|
' Please supply start_jail=True or'
|
|
' start the jail'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f'{self.jail} is not running!'
|
|
' Please supply --force (-f) or'
|
|
' start the jail'
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
try:
|
|
_prop = prop.split(".")
|
|
props = ioc_json.IOCJson(path).json_plugin_get_value(
|
|
_prop
|
|
)
|
|
except ioc_exceptions.CommandNeedsRoot as err:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION',
|
|
'message': err.message
|
|
},
|
|
_callback=self.callback,
|
|
silent=False)
|
|
|
|
if isinstance(props, dict):
|
|
return json.dumps(props, indent=4)
|
|
else:
|
|
return props
|
|
elif prop == "all":
|
|
_props = {}
|
|
|
|
props = ioc_json.IOCJson(path).json_get_value(prop)
|
|
|
|
# We want this sorted below, so we add it to the old dict
|
|
props["state"] = state
|
|
|
|
for key in sorted(props.keys()):
|
|
_props[key] = props[key]
|
|
|
|
return _props
|
|
else:
|
|
try:
|
|
return ioc_json.IOCJson(path).json_get_value(prop)
|
|
except KeyError:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{prop} is not a valid property!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
else:
|
|
jail_list = []
|
|
active_jails = ioc_common.get_active_jails()
|
|
|
|
for uuid, path in self.jails.items():
|
|
try:
|
|
jid = active_jails.get(f'ioc-{uuid.replace(".", "_")}', {}).get('jid')
|
|
state = "up" if jid else "down"
|
|
|
|
if prop == "state":
|
|
jail_list.append({uuid: state})
|
|
elif prop == "all":
|
|
_props = {}
|
|
try:
|
|
props = ioc_json.IOCJson(path).json_get_value(prop)
|
|
except (Exception, SystemExit):
|
|
# Jail is corrupt, we want all the keys to exist.
|
|
# So we will take the defaults and let the user
|
|
# know that they are not correct.
|
|
def_props = ioc_json.IOCJson().json_get_value(
|
|
'all',
|
|
default=True
|
|
)
|
|
jail_list.append({
|
|
uuid: {
|
|
**{x: 'N/A' for x in def_props},
|
|
'host_hostuuid': uuid,
|
|
'state': 'CORRUPT',
|
|
'release': 'N/A',
|
|
'jid': None,
|
|
}
|
|
})
|
|
|
|
continue
|
|
|
|
# We want this sorted below, so we add it to the old
|
|
# dict
|
|
props.update({
|
|
'state': state,
|
|
'jid': jid,
|
|
})
|
|
|
|
for key in sorted(props.keys()):
|
|
_props[key] = props[key]
|
|
|
|
jail_list.append({uuid: _props})
|
|
else:
|
|
jail_list.append({
|
|
uuid:
|
|
ioc_json.IOCJson(path).json_get_value(prop)
|
|
})
|
|
except KeyError:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{prop} is not a valid property!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
sort = ioc_common.ioc_sort("get", "key")
|
|
jail_list.sort(key=sort)
|
|
|
|
return jail_list
|
|
|
|
def import_(self, compression_algo='zip', path=None):
|
|
"""Imports a jail"""
|
|
ioc_image.IOCImage().import_jail(
|
|
self.jail, compression_algo=compression_algo, path=path
|
|
)
|
|
|
|
def list(
|
|
self, lst_type, header=False, long=False, sort='name', uuid=None,
|
|
plugin=False, quick=False, **kwargs
|
|
):
|
|
"""Returns a list of lst_type"""
|
|
|
|
if lst_type == "jid":
|
|
return ioc_list.IOCList(**kwargs).list_get_jid(uuid)
|
|
|
|
return ioc_list.IOCList(
|
|
lst_type,
|
|
header,
|
|
long,
|
|
sort,
|
|
plugin=plugin,
|
|
quick=quick,
|
|
silent=self.silent,
|
|
**kwargs
|
|
).list_datasets()
|
|
|
|
def rename(self, new_name):
|
|
uuid, old_mountpoint = self.__check_jail_existence__()
|
|
|
|
_template = False
|
|
_folders = ["jails", "templates"]
|
|
|
|
if old_mountpoint.startswith(f"{self.iocroot}/templates/"):
|
|
_template = True
|
|
_folders = _folders[::-1]
|
|
|
|
new_mountpoint = f"{self.iocroot}/{_folders[0]}/{new_name}"
|
|
|
|
if ioc_common.match_to_dir(self.iocroot, new_name,
|
|
old_uuid=old_mountpoint):
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"Jail: {new_name} already exists!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
path = f"{self.pool}/iocage/{_folders[0]}/{uuid}"
|
|
new_path = f"{self.pool}/iocage/{_folders[0]}/{new_name}"
|
|
|
|
_silent = self.silent
|
|
self.silent = True
|
|
|
|
self.stop()
|
|
|
|
self.silent = _silent
|
|
|
|
# Can't rename when the child is in a non-global zone
|
|
for str_dataset in self.get("jail_zfs_dataset").split():
|
|
data_dataset = Dataset(f'{self.pool}/{str_dataset.strip()}')
|
|
if data_dataset.exists:
|
|
# We only do this when it exists ( keeping old behavior )
|
|
data_dataset.set_property('jailed', 'off')
|
|
|
|
for release_snap in SnapshotListableResource().release_snapshots:
|
|
if uuid == release_snap.name:
|
|
rel_ds = release_snap.dataset.name
|
|
su.check_call([
|
|
'zfs', 'rename', '-r', f'{rel_ds}@{uuid}', f'@{new_name}'
|
|
])
|
|
|
|
dataset = Dataset(path)
|
|
dataset.rename(new_path, {'force_unmount': True})
|
|
|
|
self.jail = new_name
|
|
|
|
self.silent = True
|
|
self.set(f"host_hostuuid={new_name}", rename=True)
|
|
|
|
if self.get("host_hostname") == uuid:
|
|
self.set(f"host_hostname={new_name}")
|
|
|
|
zfs_dataset = self.get("jail_zfs_dataset")
|
|
if f"iocage/jails/{uuid}" in zfs_dataset:
|
|
zfs_dataset = zfs_dataset.replace(f"iocage/jails/{uuid}",
|
|
f"iocage/jails/{new_name}")
|
|
self.set(f"jail_zfs_dataset={zfs_dataset}")
|
|
|
|
self.silent = _silent
|
|
|
|
# Templates are readonly
|
|
if _template:
|
|
# All self.set's set this back to on, this must be last
|
|
dataset.set_property('readonly', 'off')
|
|
|
|
# Adjust mountpoints in fstab
|
|
jail_fstab = f"{new_mountpoint}/fstab"
|
|
|
|
try:
|
|
with open(jail_fstab, "r") as fstab:
|
|
with ioc_common.open_atomic(jail_fstab, "w") as _fstab:
|
|
for line in fstab.readlines():
|
|
_fstab.write(line.replace(
|
|
f"{self.iocroot}/jails/{uuid}/",
|
|
f"{self.iocroot}/jails/{new_name}/"))
|
|
except OSError:
|
|
pass
|
|
|
|
if _template:
|
|
for jail, path in self.jails.items():
|
|
# Stale list and isn't relevant for our loop anyways
|
|
if jail == uuid:
|
|
continue
|
|
|
|
_json = ioc_json.IOCJson(path, silent=True)
|
|
|
|
try:
|
|
source_template = _json.json_get_value('source_template')
|
|
except KeyError:
|
|
continue
|
|
|
|
if source_template == uuid:
|
|
_json.json_set_value(f'source_template={new_name}')
|
|
|
|
dataset.set_property('readonly', 'on')
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Jail: {uuid} renamed to {new_name}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def restart(self, soft=False):
|
|
if self._all:
|
|
if not soft:
|
|
self.__jail_order__("stop")
|
|
# This gets unset each time.
|
|
self._all = True
|
|
|
|
self.__jail_order__("start")
|
|
else:
|
|
for j in self.jails:
|
|
self.jail = j
|
|
self.__soft_restart__()
|
|
else:
|
|
if not soft:
|
|
# __rc__ will set this to false for each, we want to preserve
|
|
# it
|
|
_rc = self.rc
|
|
self.stop()
|
|
|
|
self.rc = _rc
|
|
self.start()
|
|
else:
|
|
self.__soft_restart__()
|
|
|
|
def rollback(self, name):
|
|
"""Rolls back a jail and all datasets to the supplied snapshot"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all')
|
|
status, _ = self.list("jid", uuid=uuid)
|
|
|
|
if status:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"Please stop {uuid} before trying to"
|
|
" rollback!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if ioc_common.check_truthy(conf['template']):
|
|
target = f"{self.pool}/iocage/templates/{uuid}"
|
|
else:
|
|
target = f"{self.pool}/iocage/jails/{uuid}"
|
|
|
|
dataset = Dataset(target)
|
|
if not dataset.exists:
|
|
ioc_common.logit(
|
|
{'level': 'EXCEPTION', 'message': f'{target} does not exist'},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
snap = Snapshot(f'{dataset.name}@{name}')
|
|
if not snap.exists:
|
|
ioc_common.logit(
|
|
{'level': 'EXCEPTION', 'message': f'{snap} does not exist'},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
|
|
for ds in dataset.get_dependents(depth=None):
|
|
if ds.properties['type'] == 'filesystem':
|
|
Snapshot(f'{ds.name}@{name}').rollback(
|
|
{'destroy_latest': True}
|
|
)
|
|
|
|
# datasets is actually the parent.
|
|
snap.rollback({'destroy_latest': True})
|
|
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Rolled back to: {target}"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def set(self, prop, plugin=False, rename=False):
|
|
"""Sets a property for a jail or plugin"""
|
|
# The cli check prevents users changing unwanted properties. We do
|
|
# want to change a protected property with rename, so we disable that.
|
|
cli = False if rename else True
|
|
|
|
try:
|
|
key, value = prop.split("=", 1)
|
|
except ValueError:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{prop} is missing a value!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if key == "ip4_addr" or key == "ip6_addr":
|
|
# We don't want spaces here
|
|
value = value.replace(" ", "")
|
|
|
|
if self.jail == "default":
|
|
ioc_json.IOCJson().check_default_config()
|
|
default = True
|
|
else:
|
|
default = False
|
|
|
|
if default:
|
|
ioc_json.IOCJson(self.iocroot).json_set_value(prop, default=True)
|
|
return
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
iocjson = ioc_json.IOCJson(
|
|
path,
|
|
cli=cli,
|
|
callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if plugin:
|
|
_prop = prop.split(".")
|
|
iocjson.json_plugin_set_value(_prop)
|
|
|
|
return
|
|
|
|
if "template" in key:
|
|
if prop in ioc_common.construct_truthy(
|
|
'template'
|
|
) and path.startswith(
|
|
f'{self.iocroot}/templates/'
|
|
):
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{uuid} is already a template!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
elif prop in ioc_common.construct_truthy(
|
|
'template', inverse=True
|
|
) and path.startswith(
|
|
f'{self.iocroot}/jails/'
|
|
):
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{uuid} is already a jail!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
try:
|
|
# We use this to test if it's a valid property at all.
|
|
_prop = prop.partition("=")[0]
|
|
self.get(_prop)
|
|
|
|
# The actual setting of the property.
|
|
iocjson.json_set_value(prop)
|
|
except KeyError:
|
|
_prop = prop.partition("=")[0]
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{_prop} is not a valid property!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
if key == "ip6_addr":
|
|
rtsold_enable = "YES" if "accept_rtadv" in value else "NO"
|
|
ioc_common.set_rcconf(path, "rtsold_enable", rtsold_enable)
|
|
|
|
def snap_list(self, long=True, _sort="created"):
|
|
"""Gathers a list of snapshots and returns it"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all')
|
|
snap_list = []
|
|
snap_list_temp = []
|
|
snap_list_root = []
|
|
|
|
if ioc_common.check_truthy(conf['template']):
|
|
full_path = f"{self.pool}/iocage/templates/{uuid}"
|
|
else:
|
|
full_path = f"{self.pool}/iocage/jails/{uuid}"
|
|
|
|
dataset = Dataset(full_path)
|
|
|
|
for snap in dataset.snapshots_recursive():
|
|
snap_name = snap.name if not long else snap.resource_name
|
|
root_snap_name = snap.resource_name.rsplit("@")[0].split("/")[-1]
|
|
root = False
|
|
|
|
if root_snap_name == "root":
|
|
if not long:
|
|
snap_name += "/root"
|
|
|
|
root = True
|
|
elif root_snap_name != uuid:
|
|
# basejail datasets.
|
|
|
|
continue
|
|
|
|
creation = snap.properties["creation"]
|
|
used = snap.properties["used"]
|
|
referenced = snap.properties["referenced"]
|
|
|
|
snap_list_temp.append([snap_name, creation, referenced, used]) \
|
|
if not root else snap_list_root.append([snap_name, creation,
|
|
referenced, used])
|
|
|
|
for parent in snap_list_temp:
|
|
# We want the /root snapshots immediately after the parent ones
|
|
name = parent[0]
|
|
|
|
if long:
|
|
name, snap_name = parent[0].split("@")
|
|
name = f"{name}/root@{snap_name}"
|
|
|
|
for root in snap_list_root:
|
|
_name = root[0]
|
|
|
|
# Long has this already, the short comparison will fail.
|
|
root_comparison = name if long else f"{name}/root"
|
|
|
|
if root_comparison == _name:
|
|
snap_list.append(parent)
|
|
snap_list.append(root)
|
|
|
|
sort = ioc_common.ioc_sort("snaplist", _sort, data=snap_list)
|
|
snap_list.sort(key=sort)
|
|
|
|
return snap_list
|
|
|
|
def snapshot(self, name):
|
|
"""Will create a snapshot for the given jail"""
|
|
date = datetime.datetime.utcnow().strftime("%F_%T")
|
|
uuid, path = self.__check_jail_existence__()
|
|
|
|
# If they don't supply a snapshot name, we will use the date.
|
|
|
|
if not name:
|
|
name = date
|
|
|
|
# Looks like foo/iocage/jails/df0ef69a-57b6-4480-b1f8-88f7b6febbdf@BAR
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all')
|
|
|
|
if ioc_common.check_truthy(conf['template']):
|
|
target = f"{self.pool}/iocage/templates/{uuid}"
|
|
else:
|
|
target = f"{self.pool}/iocage/jails/{uuid}"
|
|
|
|
snap = Snapshot(f'{target}@{name}')
|
|
if snap.exists:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'EXCEPTION', 'force_raise': True,
|
|
'message': 'Snapshot already exists'
|
|
},
|
|
_callback=self.callback, silent=self.silent,
|
|
exception=ioc_exceptions.Exists
|
|
)
|
|
|
|
snap.create_snapshot({'recursive': True})
|
|
|
|
if not self.silent:
|
|
ioc_common.logit({
|
|
"level": "INFO",
|
|
"message": f"Snapshot: {target}@{name} created."
|
|
})
|
|
|
|
def __soft_restart__(self):
|
|
"""
|
|
Executes a soft reboot by keeping the jail network stack intact,
|
|
but executing the rc scripts.
|
|
"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all')
|
|
|
|
# These need to be a list.
|
|
exec_start = conf["exec_start"].split()
|
|
exec_stop = conf["exec_stop"].split()
|
|
exec_fib = conf["exec_fib"]
|
|
|
|
if status:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": f"Soft restarting {uuid} ({self.jail})"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
stop_cmd = [
|
|
"setfib", exec_fib, "jexec", f"ioc-{uuid.replace('.', '_')}"
|
|
] + exec_stop
|
|
su.Popen(stop_cmd, stdout=su.PIPE, stderr=su.PIPE).communicate()
|
|
|
|
su.Popen(["pkill", "-j", jid]).communicate()
|
|
start_cmd = [
|
|
"setfib", exec_fib, "jexec", f"ioc-{uuid.replace('.', '_')}"
|
|
] + exec_start
|
|
su.Popen(start_cmd, stdout=su.PIPE, stderr=su.PIPE).communicate()
|
|
ioc_json.IOCJson(path, silent=True).json_set_value(
|
|
f"last_started={datetime.datetime.utcnow().strftime('%F %T')}")
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "ERROR",
|
|
"message": f"{self.jail} is not running!"
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def start(self, jail=None, ignore_exception=False, used_ports=None):
|
|
"""Checks jails type and existence, then starts the jail"""
|
|
if self.rc or self._all:
|
|
if not jail:
|
|
self.__jail_order__("start", ignore_exception=ignore_exception)
|
|
else:
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value(
|
|
'all')
|
|
release = conf["release"]
|
|
|
|
if release != "EMPTY":
|
|
release = float(release.rsplit("-", 1)[0].rsplit("-", 1)[0])
|
|
ioc_common.check_release_newer(release, major_only=True)
|
|
|
|
err, msg = self.__check_jail_type__(conf["type"], uuid)
|
|
depends = conf["depends"].split()
|
|
|
|
if not err:
|
|
for depend in depends:
|
|
if depend != "none":
|
|
try:
|
|
self.jail = depend
|
|
_is_depend = self.is_depend
|
|
self.is_depend = True
|
|
self.start(depend)
|
|
except ioc_exceptions.JailRunning:
|
|
pass
|
|
finally:
|
|
self.is_depend = _is_depend
|
|
|
|
ioc_start.IOCStart(
|
|
uuid,
|
|
path,
|
|
silent=self.silent,
|
|
callback=self.callback,
|
|
is_depend=self.is_depend,
|
|
suppress_exception=ignore_exception,
|
|
used_ports=used_ports,
|
|
)
|
|
|
|
return False, None
|
|
else:
|
|
if jail:
|
|
return err, msg
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'ERROR',
|
|
'message': msg
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|
|
exit(1)
|
|
|
|
def stop(self, jail=None, force=False, ignore_exception=False):
|
|
"""Stops the jail."""
|
|
|
|
if self.rc or self._all:
|
|
if not jail:
|
|
self.__jail_order__("stop", ignore_exception=ignore_exception)
|
|
else:
|
|
uuid, path = self.__check_jail_existence__()
|
|
ioc_stop.IOCStop(
|
|
uuid, path, silent=self.silent,
|
|
force=force, suppress_exception=ignore_exception
|
|
)
|
|
|
|
def update_all(self, pkgs=False):
|
|
"""Runs update for all jails"""
|
|
self._all = False
|
|
for jail in self.jails:
|
|
self.jail = jail
|
|
self.update(pkgs)
|
|
|
|
def update(self, pkgs=False):
|
|
"""Updates a jail to the latest patchset."""
|
|
if self._all:
|
|
self.update_all(pkgs)
|
|
return
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(
|
|
path, silent=self.silent, stop=True).json_get_value('all')
|
|
freebsd_version = ioc_common.checkoutput(["freebsd-version"])
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
started = False
|
|
_release = conf["release"].rsplit("-", 1)[0]
|
|
release = _release if "-RELEASE" in _release else conf["release"]
|
|
_silent = self.silent
|
|
jail_type = conf["type"]
|
|
updateable = True if jail_type in (
|
|
"jail", "clonejail", "pluginv2") else False
|
|
|
|
if updateable:
|
|
date = datetime.datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
|
|
self.snapshot(
|
|
f'ioc_update_{conf["release"]}_{date}'
|
|
)
|
|
|
|
if not status:
|
|
self.silent = True
|
|
self.start()
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
started = True
|
|
self.silent = _silent
|
|
elif conf["type"] == "basejail":
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Please run \"iocage migrate\" before trying"
|
|
f" to update {uuid}"
|
|
})
|
|
elif conf["type"] == "template":
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Please convert back to a jail before trying"
|
|
f" to update {uuid}"
|
|
})
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{conf['type']} is not a supported jail type."
|
|
})
|
|
|
|
if "HBSD" in freebsd_version:
|
|
su.Popen(["hbsd-update", "-j", jid]).communicate()
|
|
|
|
if started:
|
|
self.silent = True
|
|
self.stop()
|
|
self.silent = _silent
|
|
else:
|
|
if pkgs and not (jail_type in ('plugin', 'pluginv2')):
|
|
# Let's update pkg repos first
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updating pkgs...'
|
|
})
|
|
pkg_update = su.run(
|
|
['pkg-static', '-j', jid, 'update', '-q', '-f'],
|
|
stdout=su.PIPE, stderr=su.STDOUT
|
|
)
|
|
if pkg_update.returncode:
|
|
ioc_common.logit({
|
|
'level': 'EXCEPTION',
|
|
'message': 'Failed to update pkg repositories.'
|
|
})
|
|
else:
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updated pkg repositories successfully.'
|
|
})
|
|
# This will run pkg upgrade now
|
|
ioc_create.IOCCreate(
|
|
self.jail, '', 0, pkglist=[],
|
|
silent=True, callback=self.callback
|
|
).create_install_packages(self.jail, path, repo='')
|
|
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Upgraded pkgs successfully.'
|
|
})
|
|
|
|
if jail_type == "pluginv2" or jail_type == "plugin":
|
|
# TODO: Warn about erasing all pkgs
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updating plugin...'
|
|
})
|
|
ioc_plugin.IOCPlugin(
|
|
jail=uuid,
|
|
plugin=conf['plugin_name'],
|
|
git_repository=conf['plugin_repository'],
|
|
callback=self.callback
|
|
).update(jid)
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updated plugin successfully.'
|
|
})
|
|
|
|
# Jail updates should always happen
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updating jail...'
|
|
})
|
|
|
|
is_basejail = ioc_common.check_truthy(conf['basejail'])
|
|
params = [] if is_basejail else [True, uuid]
|
|
try:
|
|
ioc_fetch.IOCFetch(
|
|
release,
|
|
callback=self.callback
|
|
).fetch_update(*params)
|
|
finally:
|
|
if not started and jail_type == 'pluginv2':
|
|
silent = self.silent
|
|
self.silent = True
|
|
self.restart()
|
|
self.silent = silent
|
|
|
|
ioc_common.logit({
|
|
'level': 'INFO',
|
|
'message': 'Updated jail successfully.'
|
|
})
|
|
|
|
if started:
|
|
self.silent = True
|
|
self.stop()
|
|
self.silent = _silent
|
|
|
|
message = f"\n{uuid} updates have been applied successfully."
|
|
ioc_common.logit(
|
|
{
|
|
"level": "INFO",
|
|
"message": message
|
|
},
|
|
_callback=self.callback,
|
|
silent=self.silent)
|
|
|
|
def upgrade_all(self, release):
|
|
"""Runs upgrade for all jails"""
|
|
self._all = False
|
|
for jail in self.jails:
|
|
self.jail = jail
|
|
self.upgrade(release)
|
|
|
|
def upgrade(self, release):
|
|
if self._all:
|
|
self.upgrade_all(release)
|
|
return
|
|
|
|
if release is not None:
|
|
_release = release.rsplit("-", 1)[0].rsplit("-", 1)[0]
|
|
ioc_common.check_release_newer(_release, major_only=True)
|
|
|
|
uuid, path = self.__check_jail_existence__()
|
|
root_path = f"{path}/root"
|
|
status, jid = self.list("jid", uuid=uuid)
|
|
conf = ioc_json.IOCJson(path).json_get_value('all')
|
|
|
|
if release is None and conf["type"] != "pluginv2":
|
|
ioc_common.logit({
|
|
"level": "EXCEPTION",
|
|
"message": "Target RELEASE is required to upgrade."
|
|
},
|
|
_callback=self.callback)
|
|
|
|
jail_release = conf["release"]
|
|
|
|
if conf["type"] != "pluginv2":
|
|
if release in jail_release:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message":
|
|
f"Jail: {uuid} is already at version {release}!"
|
|
},
|
|
_callback=self.callback)
|
|
|
|
started = False
|
|
basejail = False
|
|
plugin = False
|
|
|
|
if conf["release"] == "EMPTY":
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": "Upgrading is not supported for empty jails."
|
|
},
|
|
_callback=self.callback)
|
|
|
|
if conf["type"] == "jail":
|
|
if not status:
|
|
ioc_start.IOCStart(uuid, path, silent=True)
|
|
started = True
|
|
|
|
if ioc_common.check_truthy(conf['basejail']):
|
|
new_release = ioc_upgrade.IOCUpgrade(
|
|
release,
|
|
root_path,
|
|
callback=self.callback
|
|
).upgrade_basejail()
|
|
basejail = True
|
|
else:
|
|
new_release = ioc_upgrade.IOCUpgrade(
|
|
release,
|
|
root_path,
|
|
callback=self.callback
|
|
).upgrade_jail()
|
|
elif conf["type"] == "basejail":
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Please run \"iocage migrate\" before trying"
|
|
f" to upgrade {uuid}"
|
|
},
|
|
_callback=self.callback)
|
|
elif conf["type"] == "template":
|
|
ioc_common.logit(
|
|
{
|
|
"level":
|
|
"EXCEPTION",
|
|
"message":
|
|
"Please convert back to a jail before trying"
|
|
f" to upgrade {uuid}"
|
|
},
|
|
_callback=self.callback)
|
|
elif conf["type"] == "pluginv2":
|
|
if not status:
|
|
ioc_start.IOCStart(uuid, path, silent=True)
|
|
started = True
|
|
|
|
status, jid = self.list('jid', uuid=uuid)
|
|
new_release = ioc_plugin.IOCPlugin(
|
|
jail=uuid,
|
|
plugin=conf['plugin_name'],
|
|
git_repository=conf['plugin_repository'],
|
|
callback=self.callback
|
|
).upgrade(jid)
|
|
plugin = True
|
|
else:
|
|
ioc_common.logit(
|
|
{
|
|
"level": "EXCEPTION",
|
|
"message": f"{conf['type']} is not a supported jail type."
|
|
},
|
|
_callback=self.callback)
|
|
|
|
if started:
|
|
_silent = self.silent
|
|
self.silent = True
|
|
self.stop()
|
|
self.silent = _silent
|
|
|
|
if basejail:
|
|
_date = datetime.datetime.utcnow().strftime("%F")
|
|
msg = f"""\
|
|
\n{uuid} successfully upgraded from {jail_release} to {new_release}!
|
|
Please reboot the jail and inspect.
|
|
Remove the snapshot: ioc_upgrade_{_date} if everything is OK
|
|
"""
|
|
elif plugin:
|
|
msg = f"\n{uuid} successfully upgraded!"
|
|
else:
|
|
msg = f"\n{uuid} successfully upgraded from" \
|
|
f" {jail_release} to {new_release}!"
|
|
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': msg
|
|
},
|
|
_callback=self.callback
|
|
)
|
|
|
|
def debug(self, directory):
|
|
if directory is None:
|
|
directory = f'{self.iocroot}/debug'
|
|
|
|
ioc_debug.IOCDebug(directory).run_debug()
|
|
|
|
def snap_remove(self, snapshot):
|
|
"""Removes user supplied snapshot from jail"""
|
|
uuid, path = self.__check_jail_existence__()
|
|
conf = ioc_json.IOCJson(path, silent=self.silent).json_get_value('all')
|
|
|
|
if ioc_common.check_truthy(conf['template']):
|
|
target = f'{self.pool}/iocage/templates/{uuid}@{snapshot}'
|
|
else:
|
|
target = f'{self.pool}/iocage/jails/{uuid}@{snapshot}'
|
|
|
|
# Let's verify target exists and then destroy it, else log it
|
|
snapshot = Snapshot(target)
|
|
|
|
if not snapshot:
|
|
ioc_common.logit({
|
|
'level': 'EXCEPTION',
|
|
'message': f'Snapshot: {target} not found!'
|
|
})
|
|
else:
|
|
snapshot.destroy(recursive=True)
|
|
|
|
ioc_common.logit(
|
|
{
|
|
'level': 'INFO',
|
|
'message': f'Snapshot: {target} destroyed'
|
|
},
|
|
_callback=self.callback, silent=self.silent
|
|
)
|