Skip to content
This repository was archived by the owner on Oct 10, 2020. It is now read-only.
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 12 additions & 1 deletion Atomic/backends/_docker.py
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,9 @@ def delete_image(self, image, force=False):
assert(image is not None)
try:
return self.d.remove_image(image, force=force)
except errors.NotFound:
except errors.APIError as e:
raise ValueError(str(e))
except errors.NotFound: # pylint: disable=bad-except-order
pass
except HTTPError:
pass
Expand Down Expand Up @@ -364,6 +366,7 @@ def install(self, image, name, **kwargs):

def uninstall(self, iobject, name=None, **kwargs):
atomic = kwargs.get('atomic')
ignore = kwargs.get('ignore')
assert(isinstance(atomic, Atomic))
args = atomic.args
con_obj = None if not name else self.has_container(name)
Expand Down Expand Up @@ -395,8 +398,12 @@ def uninstall(self, iobject, name=None, **kwargs):
return 0
if cmd:
return util.check_call(cmd, env=atomic.cmd_env())

# Delete the entry in the install data
util.InstallData.delete_by_id(iobject.id, ignore=ignore)
return self.delete_image(iobject.image, force=args.force)


def validate_layer(self, layer):
pass

Expand Down Expand Up @@ -450,6 +457,10 @@ def add_string_or_list_to_list(list_item, value):
else:
return self._start(iobject, args, atomic)

if iobject.get_label('INSTALL') and not args.ignore and not util.InstallData.image_installed(iobject):
raise ValueError("The image '{}' appears to have not been installed and has an INSTALL label. You "
"should install this image first. Re-run with --ignore to bypass this "
"error.".format(iobject.name or iobject.image))
# The object is an image
command = []
if iobject.run_command:
Expand Down
11 changes: 11 additions & 0 deletions Atomic/install.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
from .util import add_opt
from .syscontainers import OSTREE_PRESENT
from Atomic.backendutils import BackendUtils
from Atomic.discovery import RegistryInspectError
from time import gmtime, strftime

try:
from . import Atomic
Expand Down Expand Up @@ -129,6 +131,15 @@ def install(self):
self.display(cmd)

if not self.args.display:
try:
name = img_obj.fq_name
except RegistryInspectError:
name = img_obj.input_name
install_data = {}
install_data[name] = {'id': img_obj.id,
'install_date': strftime("%Y-%m-%d %H:%M:%S", gmtime())
}
util.InstallData.write_install_data(install_data)
return util.check_call(cmd)

@staticmethod
Expand Down
2 changes: 1 addition & 1 deletion Atomic/uninstall.py
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ def uninstall(self):
if not img_obj:
raise ValueError(e)
be = ost
be.uninstall(img_obj, name=self.args.name, atomic=self)
be.uninstall(img_obj, name=self.args.name, atomic=self, ignore=self.args.ignore)
return 0


Expand Down
117 changes: 117 additions & 0 deletions Atomic/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@
import ipaddress
import socket
from Atomic.backends._docker_errors import NoDockerDaemon
import fcntl
import time

# Atomic Utility Module

ReturnTuple = collections.namedtuple('ReturnTuple',
Expand All @@ -29,6 +32,9 @@
ATOMIC_CONFD = os.environ.get('ATOMIC_CONFD', '/etc/atomic.d/')
ATOMIC_LIBEXEC = os.environ.get('ATOMIC_LIBEXEC', '/usr/libexec/atomic')
ATOMIC_VAR_LIB = os.environ.get('ATOMIC_VAR_LIB', '/var/lib/atomic')
if not os.path.exists(ATOMIC_VAR_LIB):
os.makedirs(ATOMIC_VAR_LIB)
ATOMIC_INSTALL_JSON = os.environ.get('ATOMIC_INSTALL_JSON', os.path.join(ATOMIC_VAR_LIB, 'install.json'))

GOMTREE_PATH = "/usr/bin/gomtree"
BWRAP_OCI_PATH = "/usr/bin/bwrap-oci"
Expand Down Expand Up @@ -794,6 +800,117 @@ def load_scan_result_file(file_name):
"""
return json.loads(open(os.path.join(file_name), "r").read())


def file_lock(func):
lock_file_name = "{}.lock".format(os.path.join(os.path.dirname(ATOMIC_INSTALL_JSON), "." + os.path.basename(ATOMIC_INSTALL_JSON)))

# Create the temporary lockfile if it doesn't exist
if not os.path.exists(lock_file_name):
open(lock_file_name, 'a').close()

install_data_file = open(lock_file_name, "r")

def get_lock():
'''
Obtains a read-only file lock on the install data
:return:
'''
time_out = 0
f_lock = False
while time_out < 10.5: # Ten second attempt to get a lock
try:
fcntl.flock(install_data_file, fcntl.LOCK_EX | fcntl.LOCK_NB)
f_lock = True
break
except IOError:
time.sleep(.5)
time_out += .5
if not f_lock:
raise ValueError("Unable to get file lock for {}".format(ATOMIC_INSTALL_JSON))

def release_lock():
fcntl.flock(install_data_file, fcntl.LOCK_UN)

def wrapper(*args, **kwargs):
get_lock()
ret = func(*args, **kwargs)
release_lock()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we do a try: finally block incase func raises an exception?

return ret

return wrapper


class InstallData(object):
if not os.path.exists(ATOMIC_INSTALL_JSON):
open(ATOMIC_INSTALL_JSON, 'a').close()

install_file_handle = open(ATOMIC_INSTALL_JSON, 'r')

@staticmethod
def _read_install_data(file_handle):
try:
return json.loads(file_handle.read())
except ValueError:
return {}

@classmethod
def _write_install_data(cls, new_data):
install_data = cls._read_install_data(cls.install_file_handle)
for x in new_data:
install_data[x] = new_data[x]
temp_file = tempfile.NamedTemporaryFile(mode='w', delete=False)
json.dump(install_data, temp_file)
temp_file.close()
shutil.move(temp_file.name, ATOMIC_INSTALL_JSON)

@classmethod
@file_lock
def read_install_data(cls):
if os.path.exists(ATOMIC_INSTALL_JSON):
read_data = cls._read_install_data(cls.install_file_handle)
return read_data
return {}

@classmethod
@file_lock
def write_install_data(cls, new_data):
cls._write_install_data(new_data)

@classmethod
def get_install_name_by_id(cls, iid, install_data=None):
if not install_data:
install_data = cls._read_install_data(cls.install_file_handle)
for installed_image in install_data:
if install_data[installed_image]['id'] == iid:
return installed_image
raise ValueError("Unable to find {} in installed image data ({}). Re-run command with -i to ignore".format(id, ATOMIC_INSTALL_JSON))

@classmethod
@file_lock
def delete_by_id(cls, iid, ignore=False):
install_data = cls._read_install_data(cls.install_file_handle)
try:
id_key = InstallData.get_install_name_by_id(iid, install_data=install_data)
except ValueError as e:
if not ignore:
raise ValueError(str(e))
return
del install_data[id_key]
return cls._write_install_data(install_data)

@classmethod
def image_installed(cls, img_object):
install_data = cls.read_install_data()
if install_data.get(img_object.id, None):
return True
if install_data.get(img_object.input_name, None):
return True
if install_data.get(img_object.name, None):
return True
if install_data.get(img_object.image, None):
return True
return False

class Decompose(object):
"""
Class for decomposing an input string in its respective parts like registry,
Expand Down
2 changes: 2 additions & 0 deletions atomic
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,8 @@ def create_parser(help_text):
help=_("show atomic version and exit"))
parser.add_argument('--debug', default=False, action='store_true',
help=_("show debug messages"))
parser.add_argument('-i', '--ignore', default=False, action='store_true',
help=_("ignore install-first requirement"))
parser.add_argument('-y', '--assumeyes', default=False, action='store_true',
help=_("automatically answer yes for all questions"))
subparser = parser.add_subparsers(help=_("commands"))
Expand Down
11 changes: 7 additions & 4 deletions atomic_dbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ def __init__(self):
self.graph = None
self.heading = False
self.hotfix = False
self.ignore = False
self.image = None
self.images = []
self.import_location = None
Expand Down Expand Up @@ -392,15 +393,16 @@ def UnmountImage(self, mountpoint):
# The Run method will run the specified image
@slip.dbus.polkit.require_auth("org.atomic.readwrite")
# Return a 0 or 1 for success. Errors result in exceptions.
@dbus.service.method("org.atomic", in_signature='ssbbas', out_signature='i')
def Run(self, image, name='', spc=False, detach=False, command=''):
@dbus.service.method("org.atomic", in_signature='ssbbbas', out_signature='i')
def Run(self, image, name='', spc=False, detach=False, ignore=False, command=''):
r = Run()
args = self.Args()
args.image = image
args.name = name if name is not '' else None
args.spc = spc
args.detach = detach
args.command = command if command is not '' else []
args.ignore = ignore
r.set_args(args)
try:
return r.run()
Expand Down Expand Up @@ -644,13 +646,14 @@ def TrustDefaultPolicy(self, default_policy):
# atomic uninstall section
# The Uninstall method will uninstall the specified image
@slip.dbus.polkit.require_auth("org.atomic.readwrite")
@dbus.service.method("org.atomic", in_signature='ssbsas', out_signature='i')
def Uninstall(self, image, name, force, storage, extra_args):
@dbus.service.method("org.atomic", in_signature='ssbsbas', out_signature='i')
def Uninstall(self, image, name, force, storage, ignore, extra_args):
i = Uninstall()
args = self.Args()
args.image = image
args.name = name if name is not '' else None
args.force = force
args.ignore = ignore
args.storage = storage if storage is not '' else None
args.extra_args = [] if not extra_args else extra_args
i.set_args(args)
Expand Down
8 changes: 4 additions & 4 deletions atomic_dbus_client.py
Original file line number Diff line number Diff line change
Expand Up @@ -118,14 +118,14 @@ def MountImage(self, src, dest, options="", live=False, shared=False):

# The Run method will create and run a container on the specified image
@polkit.enable_proxy
def Run(self, image, name=None, spc=False, detach=False, command=None):
def Run(self, image, name=None, spc=False, detach=False, ignore=False, command=None):
if not name:
name = image
if not command:
command = []
if not isinstance(command, (list, tuple)):
command = [ command ]
return self.dbus_object.Run(image, name, spc, detach, command, dbus_interface="org.atomic", timeout = 2147400)
return self.dbus_object.Run(image, name, spc, detach, ignore, command, dbus_interface="org.atomic", timeout = 2147400)

@polkit.enable_proxy
def Scan(self, scan_targets, scanner, scan_type, rootfs, _all, images, containers):
Expand Down Expand Up @@ -167,14 +167,14 @@ def TrustShow(self):
return self.dbus_object.TrustShow(dbus_interface="org.atomic")

@polkit.enable_proxy
def Uninstall(self, image, name=None, force=False, storage=None, extra_args=None):
def Uninstall(self, image, name=None, force=False, storage=None, ignore=False, extra_args=None):
if not name:
name = image
if not extra_args:
extra_args = []
if not isinstance(extra_args, (list, tuple)):
extra_args = [ extra_args ]
return self.dbus_object.Install(image, name=name, force=force, storage=storage, extra_args=extra_args, dbus_interface="org.atomic", timeout = 2147400)
return self.dbus_object.Uninstall(image, name, force, storage, ignore, extra_args, dbus_interface="org.atomic", timeout = 2147400)

@polkit.enable_proxy
def UnmountImage(self, dest):
Expand Down
2 changes: 1 addition & 1 deletion bash/atomic
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ __atomic_scanners() {

_atomic_atomic() {
local boolean_options="
--help -h --version -v --debug --assumeyes -y
--help -h --version -v --debug --assumeyes -y -i --ignore
"
case "$prev" in
$main_options_with_args_glob )
Expand Down
5 changes: 5 additions & 0 deletions test.sh
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,11 @@ if [ ! -n "${PYTHON+ }" ]; then
fi
fi

# Add images with INSTALL labels to /var/lib/atomic/install.json
INSTALL_DATA_FILE="$(pwd)/install.json"
INSTALL_DATA=`docker images --no-trunc | awk '/atomic-test-/ {printf "\"%s\": {\"install_id\": \"%s\"},\n", $1, $3}' | sed 's/sha256://g' | sed '$ s/,$//'`
echo "{$INSTALL_DATA}" > ${INSTALL_DATA_FILE}
export ATOMIC_INSTALL_JSON=$INSTALL_DATA_FILE

echo "UNIT TESTS:"

Expand Down
7 changes: 4 additions & 3 deletions tests/integration/test_dbus.py
Original file line number Diff line number Diff line change
Expand Up @@ -105,11 +105,12 @@ def test_pull_already_present(self):

@integration_test
def test_run(self):
self.dbus_object.Run('atomic-test-3', name='atomic-dbus-3')
self.dbus_object.Run('atomic-test-3', 'atomic-dbus-3', False, False, True)
self.cid = TestDBus.run_cmd('docker ps -aq -l').decode('utf-8').rstrip()
TestDBus.add_cleanup_cmd('docker rm {}'.format(self.cid))
container_inspect = json.loads(TestDBus.run_cmd('docker inspect {}'.format(self.cid)).decode('utf-8'))[0]
assert(container_inspect['Name'] == '/atomic-test-3')
print(container_inspect)
assert(container_inspect['Name'] == '/atomic-dbus-3')

@integration_test
def test_container_delete(self):
Expand Down Expand Up @@ -140,7 +141,7 @@ def test_install(self):

@integration_test
def test_uninstall(self):
results = self.dbus_object.Uninstall('dbus-test-3', '', True, '', '')
results = self.dbus_object.Uninstall('dbus-test-3', '', True, '', True, '')
TestDBus.remove_cleanup_cmd('docker rm {}'.format(self.cid))
TestDBus.remove_cleanup_cmd('docker rmi atomic-dbus-3')
try:
Expand Down
Loading