Merge "Add Commands For Docker Registry Image Cleanup"

This commit is contained in:
Zuul
2019-05-22 16:40:54 +00:00
committed by Gerrit Code Review
16 changed files with 645 additions and 10 deletions

View File

@@ -1,2 +1,2 @@
SRC_DIR="cgts-client"
TIS_PATCH_VER=64
TIS_PATCH_VER=65

View File

@@ -62,6 +62,7 @@ from cgtsclient.v1 import partition
from cgtsclient.v1 import pci_device
from cgtsclient.v1 import port
from cgtsclient.v1 import ptp
from cgtsclient.v1 import registry_image
from cgtsclient.v1 import remotelogging
from cgtsclient.v1 import route
from cgtsclient.v1 import sdn_controller
@@ -143,6 +144,7 @@ class Client(http.HTTPClient):
self.sm_service = sm_service.SmServiceManager(self)
self.sm_servicegroup = sm_servicegroup.SmServiceGroupManager(self)
self.health = health.HealthManager(self)
self.registry_image = registry_image.RegistryImageManager(self)
self.remotelogging = remotelogging.RemoteLoggingManager(self)
self.sdn_controller = sdn_controller.SDNControllerManager(self)
self.partition = partition.partitionManager(self)

View File

@@ -0,0 +1,48 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
# -*- encoding: utf-8 -*-
#
from cgtsclient.common import base
from cgtsclient.v1 import options
class RegistryImage(base.Resource):
def __repr__(self):
return "<registry_image %s>" % self._info
class RegistryImageManager(base.Manager):
resource_class = RegistryImage
@staticmethod
def _path(name=None):
return '/v1/registry_image/%s' % name if name else '/v1/registry_image'
def list(self):
"""Retrieve the list of images from the registry."""
return self._list(self._path(), 'registry_images')
def tags(self, image_name):
"""Retrieve the list of tags from the registry for a specified image.
:param image_name: image name
"""
path = options.build_url(self._path(), None, ['image_name=%s' % image_name])
return self._list(path, 'registry_images')
def delete(self, image_name_and_tag):
"""Delete registry image given name and tag
:param image_name_and_tag: a string of the form name:tag
"""
path = options.build_url(self._path(), None, ['image_name_and_tag=%s' % image_name_and_tag])
return self._delete(path)
def garbage_collect(self):
path = options.build_url(self._path(), None, ['garbage_collect=%s' % True])
return self._create(path, {})

View File

@@ -0,0 +1,46 @@
#!/usr/bin/env python
# vim: tabstop=4 shiftwidth=4 softtabstop=4
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
from cgtsclient.common import utils
from cgtsclient import exc
def do_registry_image_list(cc, args):
"""List all images in local docker registry"""
images = cc.registry_image.list()
labels = ['Image Name']
fields = ['name']
utils.print_list(images, fields, labels, sortby=0)
@utils.arg('name', metavar='<image name>',
help="Name of an image")
def do_registry_image_tags(cc, args):
"""List all tags for a Docker image from the local registry"""
images = cc.registry_image.tags(args.name)
labels = ['Image Tag']
fields = ['tag']
utils.print_list(images, fields, labels, sortby=0)
@utils.arg('name_and_tag', metavar='<image name and tag>',
help="Name and tag of an image, in the form name:tag")
def do_registry_image_delete(cc, args):
"""Remove the specified Docker image from the local registry"""
try:
cc.registry_image.delete(args.name_and_tag)
print('Image %s deleted, please run garbage collect to free disk space.' % args.name_and_tag)
except exc.HTTPNotFound:
raise exc.CommandError('Image not found: %s' % args.name_and_tag)
def do_registry_garbage_collect(cc, args):
"""Run the registry garbage collector"""
cc.registry_image.garbage_collect()
print('Running docker registry garbage collect')

View File

@@ -50,6 +50,7 @@ from cgtsclient.v1 import partition_shell
from cgtsclient.v1 import pci_device_shell
from cgtsclient.v1 import port_shell
from cgtsclient.v1 import ptp_shell
from cgtsclient.v1 import registry_image_shell
from cgtsclient.v1 import remotelogging_shell
from cgtsclient.v1 import route_shell
from cgtsclient.v1 import sdn_controller_shell
@@ -106,6 +107,7 @@ COMMAND_MODULES = [
lldp_agent_shell,
lldp_neighbour_shell,
health_shell,
registry_image_shell,
remotelogging_shell,
sdn_controller_shell,
partition_shell,

View File

@@ -1,2 +1,2 @@
SRC_DIR="sysinv"
TIS_PATCH_VER=317
TIS_PATCH_VER=318

View File

@@ -59,6 +59,7 @@ from sysinv.api.controllers.v1 import port
from sysinv.api.controllers.v1 import profile
from sysinv.api.controllers.v1 import ptp
from sysinv.api.controllers.v1 import pv
from sysinv.api.controllers.v1 import registry_image
from sysinv.api.controllers.v1 import remotelogging
from sysinv.api.controllers.v1 import route
from sysinv.api.controllers.v1 import sdn_controller
@@ -223,6 +224,9 @@ class V1(base.APIBase):
health = [link.Link]
"Links to the system health resource"
registry_image = [link.Link]
"Links to the Docker registry image resource"
remotelogging = [link.Link]
"Links to the remotelogging resource"
@@ -686,6 +690,15 @@ class V1(base.APIBase):
'health', '', bookmark=True)
]
v1.registry_image = [link.Link.make_link('self',
pecan.request.host_url,
'registry_image', ''),
link.Link.make_link('bookmark',
pecan.request.host_url,
'registry_image', '',
bookmark=True)
]
v1.remotelogging = [link.Link.make_link('self',
pecan.request.host_url,
'remotelogging', ''),
@@ -809,6 +822,7 @@ class Controller(rest.RestController):
servicenodes = servicenode.SMServiceNodeController()
servicegroup = servicegroup.SMServiceGroupController()
health = health.HealthController()
registry_image = registry_image.RegistryImageController()
remotelogging = remotelogging.RemoteLoggingController()
sdn_controller = sdn_controller.SDNControllerController()
license = license.LicenseController()

View File

@@ -0,0 +1,101 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import pecan
from pecan import rest
import wsme
from wsme import types as wtypes
import wsmeext.pecan as wsme_pecan
from sysinv.api.controllers.v1 import base
from sysinv.api.controllers.v1 import collection
from sysinv.common import utils as cutils
from oslo_log import log
from sysinv.openstack.common.gettextutils import _
LOG = log.getLogger(__name__)
LOCK_NAME = 'RegistryImageController'
class RegistryImage(base.APIBase):
"""API representation of a docker registry image"""
name = wtypes.text
"The Docker image name"
tag = wtypes.text
"The Docker image tag"
def __init__(self, **kwargs):
self.fields = []
# set fields manually since Registry image comes from docker registry
# and not sysinv database
for fp in ['name', 'tag']:
self.fields.append(fp)
setattr(self, fp, kwargs.get(fp, None))
@classmethod
def convert_with_links(cls, rpc_app, expand=True):
app = RegistryImage(**rpc_app)
if not expand:
app.unset_fields_except(['name', 'tag'])
return app
class RegistryImageCollection(collection.Collection):
"""API representation of a collection of registry images."""
registry_images = [RegistryImage]
"A list containing RegistryImage objects"
def __init__(self, **kwargs):
self._type = 'registry_images'
@classmethod
def convert_with_links(cls, rpc_apps, expand=False):
collection = RegistryImageCollection()
collection.registry_images = [RegistryImage.convert_with_links(n, expand)
for n in rpc_apps]
return collection
class RegistryImageController(rest.RestController):
"""REST controller for Docker registry image."""
@wsme_pecan.wsexpose(RegistryImageCollection, wtypes.text)
def get_all(self, image_name=None):
# no image_name provided, list images
if image_name is None:
images = pecan.request.rpcapi.docker_registry_image_list(pecan.request.context)
# image_name provided, list tags of provided image
else:
images = pecan.request.rpcapi.docker_registry_image_tags(pecan.request.context, image_name)
return RegistryImageCollection.convert_with_links(images)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, wtypes.text, status_code=204)
def delete(self, image_name_and_tag):
"""Delete the image with the given name
:param name: image name and tag of the form name:tag
"""
if len(image_name_and_tag.split(":")) != 2:
raise wsme.exc.ClientSideError(_("Image name and tag must be of form name:tag"))
return pecan.request.rpcapi.docker_registry_image_delete(pecan.request.context, image_name_and_tag)
@cutils.synchronized(LOCK_NAME)
@wsme_pecan.wsexpose(None, wtypes.text)
def post(self, garbage_collect=None):
"""Run the registry garbage collector"""
if garbage_collect is not None:
pecan.request.rpcapi.docker_registry_garbage_collect(pecan.request.context)

View File

@@ -1314,6 +1314,7 @@ MURANO_CERT_KEY_FILE = os.path.join(CERT_MURANO_DIR, CERT_KEY_FILE)
MURANO_CERT_FILE = os.path.join(CERT_MURANO_DIR, CERT_FILE)
MURANO_CERT_CA_FILE = os.path.join(CERT_MURANO_DIR, CERT_CA_FILE)
DOCKER_REGISTRY_PORT = '9001'
DOCKER_REGISTRY_CERT_FILE = os.path.join(SSL_CERT_DIR, "registry-cert.crt")
DOCKER_REGISTRY_KEY_FILE = os.path.join(SSL_CERT_DIR, "registry-cert.key")
DOCKER_REGISTRY_PKCS1_KEY_FILE = os.path.join(SSL_CERT_DIR,

View File

@@ -0,0 +1,105 @@
#
# Copyright (c) 2019 Wind River Systems, Inc.
#
# SPDX-License-Identifier: Apache-2.0
#
import base64
import keyring
import requests
from sysinv.common import exception
CERT_PATH = '/etc/ssl/private/registry-cert.crt'
KEYRING_SERVICE = 'CGCS'
REGISTRY_USERNAME = 'admin'
def get_registry_password():
registry_password = keyring.get_password(
KEYRING_SERVICE, REGISTRY_USERNAME)
if not registry_password:
raise exception.DockerRegistryCredentialNotFound(
name=REGISTRY_USERNAME)
return registry_password
def docker_registry_authenticate(www_authenticate):
"""
returns a dictionary of headers to add as part of original request
including access_token
takes the Www-Authenticate header from the 401 response of a
registry request
like 'Bearer realm="https://192.168.204.2:9002/token/",
service="192.168.204.2:9001",scope="registry:catalog:*"'
:param www_authenticate: a Www-Authenticate header as described above
"""
# additional headers from the result of authentication
# for example, access_token
# send these along with the request to the docker registry
auth_headers = {'connection': 'close'}
# take off the "Bearer"
auth_params = www_authenticate.split(' ')
# unsupported www_authenticate header
if len(auth_params) != 2 or auth_params[0] != 'Bearer':
return {}
auth_params = auth_params[1].split(',')
# each auth_params should be an entry like
# service="192.168.204.2:9001"
for auth_param in auth_params:
auth_param = auth_param.split('=')
# we need to strip quotes from the auth challenge
# if we send the "scope" field in quotes, we will get
# "token intended for another audience" errors
auth_headers[auth_param[0]] = auth_param[1].strip('\"')
# 'realm' specifies a token server to authenticate to
if 'realm' not in auth_headers:
return {}
# make a request to the token server
# the credentials are passed as a header while the rest
# are passed as params
auth_string = base64.b64encode("%s:%s" % (REGISTRY_USERNAME, get_registry_password()))
token_server_request_headers = {"authorization": "Basic %s" % auth_string}
token_server_response = requests.get(auth_headers['realm'], verify=CERT_PATH,
params=auth_headers,
headers=token_server_request_headers)
if token_server_response.status_code == 200:
auth_headers['Authorization'] = "Bearer %s" % token_server_response.json().get("access_token")
return auth_headers
def docker_registry_get(path, registry_addr):
# we need to have this header to get the correct digest when giving the tag
headers = {"Accept": "application/vnd.docker.distribution.manifest.v2+json"}
resp = requests.get("%s%s" % (registry_addr, path), verify=CERT_PATH, headers=headers)
# authenticated registry, need to do auth with token server
if resp.status_code == 401:
auth_headers = docker_registry_authenticate(resp.headers["Www-Authenticate"])
headers.update(auth_headers)
resp = requests.get("%s%s" % (registry_addr, path), verify=CERT_PATH, headers=headers)
return resp
def docker_registry_delete(path, registry_addr):
headers = {}
resp = requests.delete("%s%s" % (registry_addr, path), verify=CERT_PATH, headers=headers)
# authenticated registry, need to do auth with token server
if resp.status_code == 401:
auth_headers = docker_registry_authenticate(resp.headers["Www-Authenticate"])
headers.update(auth_headers)
resp = requests.delete("%s%s" % (registry_addr, path), verify=CERT_PATH, headers=headers)
return resp

View File

@@ -64,7 +64,6 @@ TARFILE_TRANSFER_CHUNK_SIZE = 1024 * 512
DOCKER_REGISTRY_USER = 'admin'
DOCKER_REGISTRY_SERVICE = 'CGCS'
DOCKER_REGISTRY_SECRET = 'default-registry-key'
DOCKER_REGISTRY_PORT = '9001'
# Helper functions
@@ -1500,7 +1499,7 @@ class DockerHelper(object):
cutils.format_address_name(constants.CONTROLLER_HOSTNAME,
constants.NETWORK_TYPE_MGMT)
).address
registry_server = '{}:{}'.format(registry_ip, DOCKER_REGISTRY_PORT)
registry_server = '{}:{}'.format(registry_ip, constants.DOCKER_REGISTRY_PORT)
return registry_server
def _get_img_tag_with_registry(self, pub_img_tag):

View File

@@ -85,6 +85,7 @@ from cephclient import wrapper as ceph
from sysinv.conductor import ceph as iceph
from sysinv.conductor import kube_app
from sysinv.conductor import openstack
from sysinv.conductor import docker_registry
from sysinv.db import api as dbapi
from sysinv.objects import base as objects_base
from sysinv.objects import kube_app as kubeapp_obj
@@ -1429,6 +1430,104 @@ class ConductorManager(service.PeriodicService):
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
def _get_docker_registry_addr(self):
registry_ip = self.dbapi.address_get_by_name(
cutils.format_address_name(constants.CONTROLLER_HOSTNAME,
constants.NETWORK_TYPE_MGMT)
).address
registry_server = 'https://{}:{}/v2/'.format(
registry_ip, constants.DOCKER_REGISTRY_PORT)
return registry_server
def docker_registry_image_list(self, context):
image_list_response = docker_registry.docker_registry_get(
"_catalog", self._get_docker_registry_addr())
if image_list_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% image_list_response.status_code)
return []
image_list_response = image_list_response.json()
images = []
# responses from the registry looks like this
# {u'repositories': [u'meliodas/satesatesate', ...]}
# we need to turn that into what we want to return:
# [{'name': u'meliodas/satesatesate'}]
if 'repositories' not in image_list_response:
return images
image_list_response = image_list_response['repositories']
for image in image_list_response:
images.append({'name': image})
return images
def docker_registry_image_tags(self, context, image_name):
image_tags_response = docker_registry.docker_registry_get(
"%s/tags/list" % image_name, self._get_docker_registry_addr())
if image_tags_response.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% image_tags_response.status_code)
return []
image_tags_response = image_tags_response.json()
tags = []
if 'tags' not in image_tags_response:
return tags
image_tags_response = image_tags_response['tags']
# in the case where all tags of an image is deleted but not
# garbage collected
# the response will contain "tags:null"
if image_tags_response is not None:
for tag in image_tags_response:
tags.append({'name': image_name, 'tag': tag})
return tags
# assumes image_name_and_tag is already error checked to contain "name:tag"
def docker_registry_image_delete(self, context, image_name_and_tag):
image_name_and_tag = image_name_and_tag.split(":")
# first get the image digest for the image name and tag provided
digest_resp = docker_registry.docker_registry_get("%s/manifests/%s"
% (image_name_and_tag[0], image_name_and_tag[1]),
self._get_docker_registry_addr())
if digest_resp.status_code != 200:
LOG.error("Bad response from docker registry: %s"
% digest_resp.status_code)
return
image_digest = digest_resp.headers['Docker-Content-Digest']
# now delete the image
image_delete_response = docker_registry.docker_registry_delete(
"%s/manifests/%s" % (image_name_and_tag[0], image_digest),
self._get_docker_registry_addr())
if image_delete_response.status_code != 202:
LOG.error("Bad response from docker registry: %s"
% digest_resp.status_code)
return
def docker_registry_garbage_collect(self, context):
"""Run garbage collector"""
active_controller = utils.HostHelper.get_active_controller(self.dbapi)
personalities = [constants.CONTROLLER]
config_uuid = self._config_update_hosts(context, personalities,
[active_controller.uuid])
config_dict = {
"personalities": personalities,
"host_uuids": [active_controller.uuid],
"classes": ['platform::dockerdistribution::garbagecollect']
}
self._config_apply_runtime_manifest(context, config_uuid, config_dict)
def get_magnum_cluster_count(self, context):
return self._openstack.get_magnum_cluster_count()

View File

@@ -843,6 +843,39 @@ class ConductorAPI(sysinv.openstack.common.rpc.proxy.RpcProxy):
return self.call(context,
self.make_msg('update_remotelogging_config'), timeout=timeout)
def docker_registry_image_list(self, context):
"""Synchronously, request a list of images from Docker Registry API
:param context: request context.
"""
return self.call(context,
self.make_msg('docker_registry_image_list'))
def docker_registry_image_tags(self, context, image_name):
"""Synchronously, request a list of tags from Docker Registry API for a given image
:param context: request context.
"""
return self.call(context,
self.make_msg('docker_registry_image_tags', image_name=image_name))
def docker_registry_image_delete(self, context, image_name_and_tag):
"""Synchronously, delete the given image tag from the local docker registry
:param context: request context.
"""
return self.call(context,
self.make_msg('docker_registry_image_delete',
image_name_and_tag=image_name_and_tag))
def docker_registry_garbage_collect(self, context):
"""Asynchronously, run the docker registry garbage collector
:param context: request context.
"""
return self.cast(context,
self.make_msg('docker_registry_garbage_collect'))
def get_magnum_cluster_count(self, context):
"""Synchronously, have the conductor get magnum cluster count
configuration.