Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Patch/group 5 #201

Merged
merged 14 commits into from
Feb 13, 2019
Merged
253 changes: 139 additions & 114 deletions pyouroboros/dockerclient.py
Original file line number Diff line number Diff line change
Expand Up @@ -68,6 +68,65 @@ def __init__(self, docker_client):

self.monitored = self.monitor_filter()

# Container sub functions
def stop(self, container):
self.logger.debug('Stopping container: %s', container.name)
stop_signal = container.labels.get('com.ouroboros.stop_signal', False)
if stop_signal:
try:
container.kill(signal=stop_signal)
except APIError as e:
self.logger.error('Cannot kill container using signal %s. stopping normally. Error: %s',
stop_signal, e)
container.stop()
else:
container.stop()

def remove(self, container):
self.logger.debug('Removing container: %s', container.name)
try:
container.remove()
except NotFound as e:
self.logger.error("Could not remove container. Error: %s", e)
return

def recreate(self, container, latest_image):
new_config = set_properties(old=container, new=latest_image)

self.stop(container)
self.remove(container)

created = self.client.api.create_container(**new_config)
new_container = self.client.containers.get(created.get("Id"))

# connect the new container to all networks of the old container
for network_name, network_config in container.attrs['NetworkSettings']['Networks'].items():
network = self.client.networks.get(network_config['NetworkID'])
try:
network.disconnect(new_container.id, force=True)
except APIError:
pass
new_network_config = {
'container': new_container,
'aliases': network_config['Aliases'],
'links': network_config['Links'],
'ipv4_address': network_config['IPAddress'],
'ipv6_address': network_config['GlobalIPv6Address']
}
try:
network.connect(**new_network_config)
except APIError as e:
if any(err in str(e) for err in ['user configured subnets', 'user defined networks']):
if new_network_config.get('ipv4_address'):
del new_network_config['ipv4_address']
if new_network_config.get('ipv6_address'):
del new_network_config['ipv6_address']
network.connect(**new_network_config)
else:
self.logger.error('Unable to attach updated container to network "%s". Error: %s', network.name, e)

new_container.start()

def pull(self, image_object):
"""Docker pull image tag/latest"""
image = image_object
Expand Down Expand Up @@ -116,7 +175,8 @@ def pull(self, image_object):
self.logger.critical("Couldn't pull. Skipping. Error: %s", e)
raise ConnectionError

def get_running(self):
# Filters
def running_filter(self):
"""Return running container objects list, except ouroboros itself"""
running_containers = []
try:
Expand All @@ -139,7 +199,7 @@ def get_running(self):

def monitor_filter(self):
"""Return filtered running container objects list"""
running_containers = self.get_running()
running_containers = self.running_filter()
monitored_containers = []

for container in running_containers:
Expand All @@ -162,23 +222,24 @@ def monitor_filter(self):

return monitored_containers

def update(self):
updated_count = 0
updated_container_tuples = []
depends_on_list = []
# Socket Functions
def socket_check(self):
depends_on_names = []
hard_depends_on_names = []
updateable = []
self.monitored = self.monitor_filter()

if not self.monitored:
self.logger.info('No containers are running or monitored on %s', self.socket)
return

me_list = [c for c in self.client.api.containers() if 'ouroboros' in c['Names'][0].strip('/')]
if len(me_list) > 1:
self.update_self(count=2, me_list=me_list)

for container in self.monitored:
current_image = container.image

shared_image = [uct for uct in updated_container_tuples if uct[1].id == current_image.id]
shared_image = [uct for uct in updateable if uct[1].id == current_image.id]
if shared_image:
latest_image = shared_image[0][2]
else:
Expand All @@ -187,127 +248,91 @@ def update(self):
except ConnectionError:
continue

if current_image.id != latest_image.id:
updateable.append((container, current_image, latest_image))
else:
continue

# Get container list to restart after update complete
depends_on = container.labels.get('com.ouroboros.depends_on', False)
hard_depends_on = container.labels.get('com.ouroboros.hard_depends_on', False)
if depends_on:
depends_on_names.extend([name.strip() for name in depends_on.split(',')])
if hard_depends_on:
hard_depends_on_names.extend([name.strip() for name in hard_depends_on.split(',')])

hard_depends_on_containers = []
hard_depends_on_names = list(set(hard_depends_on_names))
for name in hard_depends_on_names:
try:
hard_depends_on_containers.append(self.client.containers.get(name))
except NotFound:
self.logger.error("Could not find dependant container %s on socket %s. Ignoring", name, self.socket)

depends_on_containers = []
depends_on_names = list(set(depends_on_names))
depends_on_names = [name for name in depends_on_names if name not in hard_depends_on_names]
for name in depends_on_names:
try:
depends_on_containers.append(self.client.containers.get(name))
except NotFound:
self.logger.error("Could not find dependant container %s on socket %s. Ignoring", name, self.socket)

return updateable, depends_on_containers, hard_depends_on_containers

def update(self):
updated_count = 0
try:
updateable, depends_on_containers, hard_depends_on_containers = self.socket_check()
except TypeError:
return

for container in depends_on_containers + hard_depends_on_containers:
self.stop(container)

for container, current_image, latest_image in updateable:
if self.config.dry_run:
# Ugly hack for repo digest
repo_digest_id = current_image.attrs['RepoDigests'][0].split('@')[1]
if repo_digest_id != latest_image.id:
self.logger.info('dry run : %s would be updated', container.name)
continue

# If current running container is running latest image
if current_image.id != latest_image.id:
updated_container_tuples.append(
(container, current_image, latest_image)
)
if container.name in ['ouroboros', 'ouroboros-updated']:
self.data_manager.total_updated[self.socket] += 1
self.data_manager.add(label=container.name, socket=self.socket)
self.data_manager.add(label='all', socket=self.socket)
self.notification_manager.send(container_tuples=updateable,
socket=self.socket, kind='update')
self.update_self(old_container=container, new_image=latest_image, count=1)

if container.name in ['ouroboros', 'ouroboros-updated']:
self.data_manager.total_updated[self.socket] += 1
self.data_manager.add(label=container.name, socket=self.socket)
self.data_manager.add(label='all', socket=self.socket)
self.notification_manager.send(container_tuples=updated_container_tuples,
socket=self.socket, kind='update')
self.update_self(old_container=container, new_image=latest_image, count=1)

self.logger.info('%s will be updated', container.name)

# Get container list to restart after update complete
depends_on = container.labels.get('com.ouroboros.depends-on', False)
if depends_on:
depends_on_list.extend([name.strip() for name in depends_on.split(',')])
# new container dict to create new container from
new_config = set_properties(old=container, new=latest_image)

self.logger.debug('Stopping container: %s', container.name)
stop_signal = container.labels.get('com.ouroboros.stop-signal', False)
if stop_signal:
try:
container.kill(signal=stop_signal)
except APIError as e:
self.logger.error('Cannot kill container using signal %s. stopping normally. Error: %s',
stop_signal, e)
container.stop()
else:
container.stop()
self.logger.info('%s will be updated', container.name)

self.logger.debug('Removing container: %s', container.name)
self.recreate(container, latest_image)

if self.config.cleanup:
try:
container.remove()
except NotFound as e:
self.logger.error("Could not remove container. Error: %s", e)

created = self.client.api.create_container(**new_config)
new_container = self.client.containers.get(created.get("Id"))

# disconnect new container from old networks (with possible
# broken config)
for network in new_container.attrs['NetworkSettings']['Networks']:
self.client.api.disconnect_container_from_network(
container=new_container.attrs['Id'],
net_id=network,
force=True
)

# connect the new container to all networks of the old container
for network in container.attrs['NetworkSettings']['Networks']:
try:
# assuming the network has user configured subnet
self.client.api.connect_container_to_network(
container=new_container.attrs['Id'],
net_id=network,
aliases=container.attrs['NetworkSettings']['Networks'][network]['Aliases'],
links=container.attrs['NetworkSettings']['Networks'][network]['Links'],
ipv4_address=container.attrs['NetworkSettings']['Networks'][network]['IPAddress'],
ipv6_address=container.attrs['NetworkSettings']['Networks'][network]['GlobalIPv6Address']
)
except APIError as e:
if ('user specified IP address is supported only when '
'connecting to networks with user configured subnets' in str(e)):
# configure the network without ip addresses
try:
self.client.api.connect_container_to_network(
container=new_container.attrs['Id'],
net_id=network,
aliases=container.attrs['NetworkSettings']['Networks'][network]['Aliases'],
links=container.attrs['NetworkSettings']['Networks'][network]['Links']
)
except APIError as e:
self.logger.error(
'Unable to attach updated container to network "%s". Error: %s', network, e
)
else:
# another exception occured
raise

new_container.start()

if self.config.cleanup:
try:
self.client.images.remove(current_image.id)
except APIError as e:
self.logger.error("Could not delete old image for %s, Error: %s", container.name, e)
updated_count += 1
self.client.images.remove(current_image.id)
except APIError as e:
self.logger.error("Could not delete old image for %s, Error: %s", container.name, e)
updated_count += 1

self.logger.debug("Incrementing total container updated count")
self.logger.debug("Incrementing total container updated count")

self.data_manager.total_updated[self.socket] += 1
self.data_manager.add(label=container.name, socket=self.socket)
self.data_manager.add(label='all', socket=self.socket)
self.data_manager.total_updated[self.socket] += 1
self.data_manager.add(label=container.name, socket=self.socket)
self.data_manager.add(label='all', socket=self.socket)

if depends_on_list:
depends_on_containers = []
for name in list(set(depends_on_list)):
try:
depends_on_containers.append(self.client.containers.get(name))
except NotFound:
self.logger.error("Could not find dependant container %s on socket %s. Ignoring", name, self.socket)
for container in depends_on_containers:
# Reload container to ensure it isn't referencing the old image
container.reload()
container.start()

if depends_on_containers:
for container in depends_on_containers:
self.logger.debug('Restarting dependant container %s', container.name)
container.restart()
for container in hard_depends_on_containers:
self.recreate(container, container.image)

if updated_count > 0:
self.notification_manager.send(container_tuples=updated_container_tuples, socket=self.socket, kind='update')
self.notification_manager.send(container_tuples=updateable, socket=self.socket, kind='update')

def update_self(self, count=None, old_container=None, me_list=None, new_image=None):
if count == 2:
Expand Down Expand Up @@ -430,7 +455,7 @@ def update(self):
self.data_manager.add(label=service.name, socket=self.socket)
self.data_manager.add(label='all', socket=self.socket)
self.notification_manager.send(container_tuples=updated_service_tuples,
socket=self.socket, kind='update')
socket=self.socket, kind='update', mode='service')

self.logger.info('%s will be updated', service.name)
service.update(image=tag)
Expand Down
4 changes: 1 addition & 3 deletions pyouroboros/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,7 @@ def set_properties(old, new, self_name=None):
'host_config': old.attrs['HostConfig'],
'labels': old.attrs['Config']['Labels'],
'entrypoint': old.attrs['Config']['Entrypoint'],
'environment': old.attrs['Config']['Env'],
# networks are conigured later
'networking_config': None
'environment': old.attrs['Config']['Env']
}

return properties
4 changes: 2 additions & 2 deletions pyouroboros/notifiers.py
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@ def build_apprise(self):

return apprise_obj

def send(self, container_tuples=None, socket=None, kind='update', next_run=None):
def send(self, container_tuples=None, socket=None, kind='update', next_run=None, mode='container'):
if kind == 'startup':
now = datetime.now(timezone.utc).astimezone()
title = f'Ouroboros has started'
Expand All @@ -52,7 +52,7 @@ def send(self, container_tuples=None, socket=None, kind='update', next_run=None)
[
"{} updated from {} to {}".format(
container.name,
old_image.short_id.split(':')[1],
old_image if mode == 'service' else old_image.short_id.split(':')[1],
new_image.short_id.split(':')[1]
) for container, old_image, new_image in container_tuples
]
Expand Down