Merge "Add capsule method in zunclient"

This commit is contained in:
Jenkins
2017-09-23 03:57:26 +00:00
committed by Gerrit Code Review
12 changed files with 547 additions and 11 deletions

View File

@@ -11,3 +11,4 @@ oslo.i18n>=3.15.3 # Apache-2.0
oslo.utils>=3.28.0 # Apache-2.0 oslo.utils>=3.28.0 # Apache-2.0
websocket-client>=0.33.0 # LGPLv2+ websocket-client>=0.33.0 # LGPLv2+
docker>=2.4.2 # Apache-2.0 docker>=2.4.2 # Apache-2.0
PyYAML>=3.10 # MIT

View File

@@ -340,7 +340,6 @@ class SessionClient(adapter.LegacyJsonAdapter):
endpoint_filter.setdefault('interface', self.interface) endpoint_filter.setdefault('interface', self.interface)
endpoint_filter.setdefault('service_type', self.service_type) endpoint_filter.setdefault('service_type', self.service_type)
endpoint_filter.setdefault('region_name', self.region_name) endpoint_filter.setdefault('region_name', self.region_name)
resp = self.session.request(url, method, resp = self.session.request(url, method,
raise_exc=False, **kwargs) raise_exc=False, **kwargs)

View File

@@ -0,0 +1,63 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
import json
import yaml
from zunclient.i18n import _
yaml_loader = yaml.SafeLoader
if hasattr(yaml, 'CSafeDumper'):
yaml_dumper = yaml.CSafeDumper
else:
yaml_dumper = yaml.SafeDumper
def _construct_yaml_str(self, node):
# Override the default string handling function
# to always return unicode objects
return self.construct_scalar(node)
yaml_loader.add_constructor(u'tag:yaml.org,2002:str', _construct_yaml_str)
# Unquoted dates like 2013-05-23 in yaml files get loaded as objects of type
# datetime.data which causes problems in API layer when being processed by
# openstack.common.jsonutils. Therefore, make unicode string out of timestamps
# until jsonutils can handle dates.
yaml_loader.add_constructor(u'tag:yaml.org,2002:timestamp',
_construct_yaml_str)
def parse(tmpl_str):
"""Takes a string and returns a dict containing the parsed structure.
This includes determination of whether the string is using the
JSON or YAML format.
"""
# strip any whitespace before the check
tmpl_str = tmpl_str.strip()
if tmpl_str.startswith('{'):
tpl = json.loads(tmpl_str)
else:
try:
tpl = yaml.safe_load(tmpl_str)
except yaml.YAMLError as yea:
raise ValueError(yea)
else:
if tpl is None:
tpl = {}
# Looking for supported version keys in the loaded template
if not ('CapsuleTemplateFormatVersion' in tpl
or 'capsule_template_version' in tpl):
raise ValueError(_("Template format version not found."))
return tpl

View File

@@ -0,0 +1,87 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from oslo_serialization import jsonutils
import six
from six.moves.urllib import parse
from six.moves.urllib import request
from zunclient.common import template_format
from zunclient.common import utils
from zunclient import exceptions
from zunclient.i18n import _
def get_template_contents(template_file=None, template_url=None,
files=None):
# Transform a bare file path to a file:// URL.
if template_file: # nosec
template_url = utils.normalise_file_path_to_url(template_file)
tpl = request.urlopen(template_url).read()
else:
raise exceptions.CommandErrorException(_('Need to specify exactly '
'one of %(arg1)s, %(arg2)s '
'or %(arg3)s') %
{'arg1': '--template-file',
'arg2': '--template-url'})
if not tpl:
raise exceptions.CommandErrorException(_('Could not fetch '
'template from %s') %
template_url)
try:
if isinstance(tpl, six.binary_type):
tpl = tpl.decode('utf-8')
template = template_format.parse(tpl)
except ValueError as e:
raise exceptions.CommandErrorException(_('Error parsing template '
'%(url)s %(error)s') %
{'url': template_url,
'error': e})
return template
def is_template(file_content):
try:
if isinstance(file_content, six.binary_type):
file_content = file_content.decode('utf-8')
template_format.parse(file_content)
except (ValueError, TypeError):
return False
return True
def get_file_contents(from_data, files, base_url=None,
ignore_if=None):
if isinstance(from_data, dict):
for key, value in from_data.items():
if ignore_if and ignore_if(key, value):
continue
if base_url and not base_url.endswith('/'):
base_url = base_url + '/'
str_url = parse.urljoin(base_url, value)
if str_url not in files:
file_content = utils.read_url_content(str_url)
if is_template(file_content):
template = get_template_contents(
template_url=str_url, files=files)[1]
file_content = jsonutils.dumps(template)
files[str_url] = file_content
# replace the data value with the normalised absolute URL
from_data[key] = str_url

View File

@@ -15,9 +15,11 @@
# under the License. # under the License.
import json import json
import os
from oslo_utils import netutils from oslo_utils import netutils
from six.moves.urllib import parse
from six.moves.urllib import request
from zunclient.common.apiclient import exceptions as apiexec from zunclient.common.apiclient import exceptions as apiexec
from zunclient.common import cliutils as utils from zunclient.common import cliutils as utils
from zunclient import exceptions as exc from zunclient import exceptions as exc
@@ -232,3 +234,24 @@ def parse_nets(ns):
nets.append(net_info) nets.append(net_info)
return nets return nets
def normalise_file_path_to_url(path):
if parse.urlparse(path).scheme:
return path
path = os.path.abspath(path)
return parse.urljoin('file:', request.pathname2url(path))
def base_url_for_url(url):
parsed = parse.urlparse(url)
parsed_dir = os.path.dirname(parsed.path)
return parse.urljoin(url, parsed_dir)
def list_capsules(capsules):
columns = ('uuid', 'status', 'meta_name',
'meta_labels', 'containers_uuids', 'created_at', 'addresses')
utils.print_list(capsules, columns,
{'versions': print_list_field('versions')},
sortby_index=None)

View File

@@ -23,6 +23,7 @@ HTTPBadRequest = BadRequest
HTTPInternalServerError = InternalServerError HTTPInternalServerError = InternalServerError
HTTPNotFound = NotFound HTTPNotFound = NotFound
HTTPServiceUnavailable = ServiceUnavailable HTTPServiceUnavailable = ServiceUnavailable
CommandErrorException = CommandError
class AmbiguousAuthSystem(ClientException): class AmbiguousAuthSystem(ClientException):

View File

View File

@@ -0,0 +1,106 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from zunclient.common import base
from zunclient.common import utils
from zunclient import exceptions
CREATION_ATTRIBUTES = ['spec']
class Capsule(base.Resource):
def __repr__(self):
return "<Capsule %s>" % self._info
class CapsuleManager(base.Manager):
resource_class = Capsule
@staticmethod
def _path(id=None):
if id:
return '/capsules/%s' % id
else:
return '/capsules/'
def get(self, id):
try:
return self._list(self._path(id))[0]
except IndexError:
return None
def create(self, **kwargs):
new = {}
for (key, value) in kwargs.items():
if key in CREATION_ATTRIBUTES:
new[key] = value
else:
raise exceptions.InvalidAttribute(
"Key must be in %s" % ','.join(CREATION_ATTRIBUTES))
return self._create(self._path(), new)
def list(self, marker=None, limit=None, sort_key=None,
sort_dir=None, detail=False, all_tenants=False):
"""Retrieve a list of capsules.
:param all_tenants: Optional, list containers in all tenants
:param marker: Optional, the UUID of a containers, eg the last
containers from a previous result set. Return
the next result set.
:param limit: The maximum number of results to return per
request, if:
1) limit > 0, the maximum number of containers to return.
2) limit == 0, return the entire list of containers.
3) limit param is NOT specified (None), the number of items
returned respect the maximum imposed by the ZUN API
(see Zun's api.max_limit option).
:param sort_key: Optional, field used for sorting.
:param sort_dir: Optional, direction of sorting, either 'asc' (the
default) or 'desc'.
:param detail: Optional, boolean whether to return detailed information
about containers.
:returns: A list of containers.
"""
if limit is not None:
limit = int(limit)
filters = utils.common_filters(marker, limit, sort_key,
sort_dir, all_tenants)
path = ''
if detail:
path += 'detail'
if filters:
path += '?' + '&'.join(filters)
if limit is None:
return self._list(self._path(path),
"capsules")
else:
return self._list_pagination(self._path(path),
"capsules",
limit=limit)
def delete(self, id, force):
return self._delete(self._path(id),
qparams={'force': force})

View File

@@ -0,0 +1,95 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain
# a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
# WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
# License for the specific language governing permissions and limitations
# under the License.
from zunclient.common import cliutils as utils
from zunclient.common import template_utils
from zunclient.common import utils as zun_utils
from zunclient.i18n import _
def _show_capsule(capsule):
utils.print_dict(capsule._info)
@utils.arg('-f', '--template-file', metavar='<file>',
required=True, help=_('Path to the template.'))
def do_capsule_create(cs, args):
"""Create a capsule.
Add '--experimental-api' due to capsule now is the experimental API
"""
opts = {}
if args.template_file:
template = template_utils.get_template_contents(
args.template_file)
opts['spec'] = template
cs.capsules.create(**opts)
@utils.arg('--all-tenants',
action="store_true",
default=False,
help='List containers in all tenants')
@utils.arg('--marker',
metavar='<marker>',
default=None,
help='The last container UUID of the previous page; '
'displays list of containers after "marker".')
@utils.arg('--limit',
metavar='<limit>',
type=int,
help='Maximum number of containers to return')
@utils.arg('--sort-key',
metavar='<sort-key>',
help='Column to sort results by')
@utils.arg('--sort-dir',
metavar='<sort-dir>',
choices=['desc', 'asc'],
help='Direction to sort. "asc" or "desc".')
def do_capsule_list(cs, args):
"""Print a list of available capsules.
Add '--experimental-api' due to capsule now is the experimental API
"""
opts = {}
opts['all_tenants'] = args.all_tenants
opts['marker'] = args.marker
opts['limit'] = args.limit
opts['sort_key'] = args.sort_key
opts['sort_dir'] = args.sort_dir
opts = zun_utils.remove_null_parms(**opts)
capsules = cs.capsules.list(**opts)
zun_utils.list_capsules(capsules)
@utils.arg('capsules',
metavar='<capsule>',
nargs='+',
help='ID or name of the (capsule)s to delete.')
@utils.arg('-f', '--force',
action='store_true',
help='Force delete the capsule.')
def do_capsule_delete(cs, args):
"""Delete specified capsules.
Add '--experimental-api' due to capsule now is the experimental API
"""
for capsule in args.capsules:
try:
cs.capsules.delete(capsule, args.force)
print("Request to delete capsule %s has been accepted." %
capsule)
except Exception as e:
print("Delete for capsule %(capsule)s failed: %(e)s" %
{'capsule': capsule, 'e': e})

View File

@@ -0,0 +1,123 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain a
# copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from keystoneauth1 import loading
from keystoneauth1 import session as ksa_session
from oslo_utils import importutils
from zunclient.common import httpclient
from zunclient.experimental import capsules
profiler = importutils.try_import("osprofiler.profiler")
class Client(object):
def __init__(self, username=None, api_key=None, project_id=None,
project_name=None, auth_url=None, zun_url=None,
endpoint_type=None, endpoint_override=None,
service_type='container-experimental',
region_name=None, input_auth_token=None,
session=None, password=None, auth_type='password',
interface='public', service_name=None, insecure=False,
user_domain_id=None, user_domain_name=None,
project_domain_id=None, project_domain_name=None,
api_version=None, **kwargs):
# We have to keep the api_key are for backwards compat, but let's
# remove it from the rest of our code since it's not a keystone
# concept
if not password:
password = api_key
# Backwards compat for people assing in endpoint_type
if endpoint_type:
interface = endpoint_type
# fix (yolanda): os-cloud-config is using endpoint_override
# instead of zun_url
if endpoint_override and not zun_url:
zun_url = endpoint_override
if zun_url and input_auth_token:
auth_type = 'admin_token'
session = None
loader_kwargs = dict(
token=input_auth_token,
endpoint=zun_url)
elif input_auth_token and not session:
auth_type = 'token'
loader_kwargs = dict(
token=input_auth_token,
auth_url=auth_url,
project_id=project_id,
project_name=project_name,
user_domain_id=user_domain_id,
user_domain_name=user_domain_name,
project_domain_id=project_domain_id,
project_domain_name=project_domain_name)
else:
loader_kwargs = dict(
username=username,
password=password,
auth_url=auth_url,
project_id=project_id,
project_name=project_name,
user_domain_id=user_domain_id,
user_domain_name=user_domain_name,
project_domain_id=project_domain_id,
project_domain_name=project_domain_name)
# Backwards compatibility for people not passing in Session
if session is None:
loader = loading.get_plugin_loader(auth_type)
# This should be able to handle v2 and v3 Keystone Auth
auth_plugin = loader.load_from_options(**loader_kwargs)
session = ksa_session.Session(
auth=auth_plugin, verify=(not insecure))
client_kwargs = {}
if zun_url:
client_kwargs['endpoint_override'] = zun_url
if not zun_url:
try:
# Trigger an auth error so that we can throw the exception
# we always have
session.get_endpoint(
service_type=service_type,
service_name=service_name,
interface=interface,
region_name=region_name)
except Exception:
raise RuntimeError("Not Authorized")
self.http_client = httpclient.SessionClient(
service_type=service_type,
service_name=service_name,
interface=interface,
region_name=region_name,
session=session,
api_version=api_version,
**client_kwargs)
self.capsules = capsules.CapsuleManager(self.http_client)
profile = kwargs.pop("profile", None)
if profiler and profile:
# Initialize the root of the future trace: the created trace ID
# will be used as the very first parent to which all related
# traces will be bound to. The given HMAC key must correspond to
# the one set in zun-api zun.conf, otherwise the latter
# will fail to check the request signature and will skip
# initialization of osprofiler on the server side.
profiler.init(profile)

View File

@@ -0,0 +1,19 @@
# Copyright 2017 Arm Limited.
#
# Licensed under the Apache License, Version 2.0 (the "License"); you may
# not use this file except in compliance with the License. You may obtain a
# copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from zunclient.experimental import capsules_shell
COMMAND_MODULES = [
capsules_shell,
]

View File

@@ -53,10 +53,12 @@ except ImportError:
pass pass
from zunclient import api_versions from zunclient import api_versions
from zunclient import client from zunclient import client as base_client
from zunclient.common.apiclient import auth from zunclient.common.apiclient import auth
from zunclient.common import cliutils from zunclient.common import cliutils
from zunclient import exceptions as exc from zunclient import exceptions as exc
from zunclient.experimental import client as client_experimental
from zunclient.experimental import shell as shell_experimental
from zunclient.i18n import _ from zunclient.i18n import _
from zunclient.v1 import shell as shell_v1 from zunclient.v1 import shell as shell_v1
from zunclient import version from zunclient import version
@@ -64,6 +66,7 @@ from zunclient import version
DEFAULT_API_VERSION = api_versions.DEFAULT_API_VERSION DEFAULT_API_VERSION = api_versions.DEFAULT_API_VERSION
DEFAULT_ENDPOINT_TYPE = 'publicURL' DEFAULT_ENDPOINT_TYPE = 'publicURL'
DEFAULT_SERVICE_TYPE = 'container' DEFAULT_SERVICE_TYPE = 'container'
EXPERIENTAL_SERVICE_TYPE = 'container-experimental'
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -361,6 +364,10 @@ class OpenStackZunShell(object):
action='store_true', action='store_true',
help="Do not verify https connections") help="Do not verify https connections")
parser.add_argument('--experimental-api',
action='store_true',
help="Using experimental API")
if profiler: if profiler:
parser.add_argument('--profile', parser.add_argument('--profile',
metavar='HMAC_KEY', metavar='HMAC_KEY',
@@ -382,21 +389,22 @@ class OpenStackZunShell(object):
return parser return parser
def get_subcommand_parser(self, version, do_help=False): def get_subcommand_parser(self, version, experimental, do_help=False):
parser = self.get_base_parser() parser = self.get_base_parser()
self.subcommands = {} self.subcommands = {}
subparsers = parser.add_subparsers(metavar='<subcommand>') subparsers = parser.add_subparsers(metavar='<subcommand>')
try: try:
actions_modules = { actions_modules = shell_v1.COMMAND_MODULES
'1': shell_v1.COMMAND_MODULES if experimental:
}[version.ver_major] for items in shell_experimental.COMMAND_MODULES:
actions_modules.append(items)
except KeyError: except KeyError:
actions_modules = shell_v1.COMMAND_MODULES actions_modules = shell_v1.COMMAND_MODULES
for actions_module in actions_modules: for action_modules in actions_modules:
self._find_actions(subparsers, actions_module, version, do_help) self._find_actions(subparsers, action_modules, version, do_help)
self._find_actions(subparsers, self, version, do_help) self._find_actions(subparsers, self, version, do_help)
self._add_bash_completion_subparser(subparsers) self._add_bash_completion_subparser(subparsers)
@@ -504,8 +512,12 @@ class OpenStackZunShell(object):
spot = argv.index('--endpoint_type') spot = argv.index('--endpoint_type')
argv[spot] = '--endpoint-type' argv[spot] = '--endpoint-type'
experimental = False
if '--experimental-api' in argv:
experimental = True
subcommand_parser = self.get_subcommand_parser( subcommand_parser = self.get_subcommand_parser(
api_version, do_help=("help" in args)) api_version, experimental, do_help=("help" in args))
self.parser = subcommand_parser self.parser = subcommand_parser
@@ -549,6 +561,9 @@ class OpenStackZunShell(object):
if not service_type: if not service_type:
service_type = DEFAULT_SERVICE_TYPE service_type = DEFAULT_SERVICE_TYPE
if experimental:
service_type = EXPERIENTAL_SERVICE_TYPE
# NA - there is only one service this CLI accesses # NA - there is only one service this CLI accesses
# service_type = utils.get_service_type(args.func) or service_type # service_type = utils.get_service_type(args.func) or service_type
@@ -608,6 +623,11 @@ class OpenStackZunShell(object):
'--os-password, env[OS_PASSWORD], or ' '--os-password, env[OS_PASSWORD], or '
'prompted response') 'prompted response')
if experimental:
client = client_experimental
else:
client = base_client
kwargs = {} kwargs = {}
if profiler: if profiler:
kwargs["profile"] = args.profile kwargs["profile"] = args.profile
@@ -671,7 +691,6 @@ class OpenStackZunShell(object):
"""Display help about this program or one of its subcommands.""" """Display help about this program or one of its subcommands."""
# NOTE(jamespage): args.command is not guaranteed with python >= 3.4 # NOTE(jamespage): args.command is not guaranteed with python >= 3.4
command = getattr(args, 'command', '') command = getattr(args, 'command', '')
if command: if command:
if args.command in self.subcommands: if args.command in self.subcommands:
self.subcommands[args.command].print_help() self.subcommands[args.command].print_help()