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

Add keystone-shell container #185

Open
wants to merge 11 commits into
base: master
Choose a base branch
from
22 changes: 22 additions & 0 deletions keystone-shell/Dockerfile
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
FROM monasca/python:3-20171101-203444

# To force a rebuild, pass --build-arg REBUILD="$(DATE)" when running
# `docker build`
ARG REBUILD=1

COPY requirements.txt /

RUN apk add --no-cache ca-certificates tini libffi && \
apk add --no-cache --virtual build-dep \
musl-dev linux-headers git make g++ openssl-dev libffi-dev && \
pip install urllib3 ipaddress ipython ptpython python-openstackclient python-monascaclient && \
pip install -r /requirements.txt && \
rm -rf /root/.cache/pip && \
apk del build-dep

COPY keystone_shell_vars.py ptpython_init.py start.sh /
COPY keystonerc.sh /root/.keystonerc

ENV ENV="/root/.keystonerc"
ENTRYPOINT ["sh", "-l", "/start.sh"]

132 changes: 132 additions & 0 deletions keystone-shell/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
keystone-shell
==============

This image allows you to quickly interact with a keystone environment.

Sources: [keystone-shell][1] · [Dockerfile][2] · [monasca-docker][3]

Tags
----

Images in this repository are tagged as follows:

* `latest`: refers to the latest stable point release, e.g. `1.0.0`
* `1.0.0`, `1.0`, `1`: standard semver tags

Basic Usage
-----------

This is mainly intended for use in Kubernetes environments configured using
[keystone-init][4]. To use, run:

kubectl run keystone-shell -i -t \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest

(pass `-n <NAMESPACE>` as needed if the desired secrets are located elsewhere)

Once the container starts, press enter once to start the shell. Basic usage
information will be printed to the terminal.

As [keystone-init][4] includes all necessary environment variables inside its
secrets, no additional environment variables are needed. However, use without
this utility or in standalone Docker environments will require the necessary
`OS_` variables to be set manually.

There are three commands available:
- `secret <NAME>`: load keystone `OS_` variables from secret in current
namespace `NAME`
- `shell [NAME]`: start a Python shell (using ptipython, with highlighting
and autocomplete) with pre-connected Keystone and Kubernetes clients -
additional usage information will be printed on startup
- if specified, secret with `NAME` will be loaded first (as with
`secret NAME`) and will override existing environment variables
- `openstack ...`: the standard openstack client
- `monasca ...`: the standard Monasca client (additional configuration
needed, see below)

Direct Shell
------------

You can also start a shell directly:

kubectl run keystone-shell \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest \
--env="OS_USERNAME=admin" \
--env="OS_PASSWORD=secretadmin" \
--env="OS_PROJECT_NAME=admin" \
--env="OS_PROJECT_DOMAIN_NAME=Default" \
--env="OS_USER_DOMAIN_NAME=Default" \
--env="OS_AUTH_URL=http://monasca-keystone:35357/" \
--env="KEYSTONE_SHELL=true" \
--attach -i -t

Note the `KEYSTONE_SHELL` variable. `OS_` variables can also be provided in
lieu of a secret name (allowing the container to be run in docker rather than
Kubernetes).


Verifying Keystone Connectivity
-------------------------------

This container can be used as a one-off method to check if a Keystone instance
is available. To use, try:

kubectl run keystone-shell \
--rm=true \
--restart Never \
--image=monasca/keystone-shell:latest \
--env="KEYSTONE_SECRET=keystone-example-user" \
--attach

Note the absence of the `-i` and `-t` parameters. Note that `KEYSTONE_SECRET`
must be defined in this scenario.

If Keystone is available and the credentials work, the return code will be
zero. If Keystone is not available or the credentials are invalid, the return
code will be 1. Log information should be printed to help narrow down the
error, or at least as much as Keystone will be willing to divulge about it.

Interacting with Monasca
------------------------

The keystone-shell container includes a monasca client as well, however it may
require a small amount of manual configuration:

$ kubectl run keystone-shell -i -t \
--rm=true \
--restart Never \
--env="KEYSTONE_SECRET=keystone-example-user"
--image=monasca/keystone-shell:latest
keystone-shell:/# export MONASCA_API_URL=http://monasca-api:8070/
keystone-shell:/# export OS_IDENTITY_API_VERSION=3
keystone-shell:/# monasca ...


Note that your particular `monasca-api` may have a different service name.


Configuration
-------------

| Variable | Default | Description |
|--------------------------|---------|----------------------------------------|
| `LOG_LEVEL` | `INFO` | Python logging level |
| `KEYSTONE_TIMEOUT` | `10` | Keystone connection timeout in seconds |
| `KEYSTONE_VERIFY` | `true` | If `false`, don't verify SSL |
| `KEYSTONE_CERT` | unset | Path to mounted alternative CA bundle |
| `KEYSTONE_SECRET` | unset | Secret to auto-load on startup |
| `OS_AUTH_URL` | unset | (**required**) |
| `OS_USERNAME` | unset | keystone username |
| `OS_PASSWORD` | unset | keystone password |
| `OS_USER_DOMAIN_NAME` | unset | keystone user domain name |
| `OS_PROJECT_NAME` | unset | keystone project name |
| `OS_PROJECT_DOMAIN_NAME` | unset | keystone project domain name |

[1]: https://github.com/monasca/monasca-docker/blob/master/keystone-shell/
[2]: https://github.com/monasca/monasca-docker/blob/master/keystone-shell/Dockerfile
[3]: https://github.com/monasca/monasca-docker/
[4]: https://github.com/monasca/monasca-docker/blob/master/keystone-init/
7 changes: 7 additions & 0 deletions keystone-shell/build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
repository: monasca/keystone-shell
variants:
- tag: latest
aliases:
- :1.0.0
- :1.0
- :1
151 changes: 151 additions & 0 deletions keystone-shell/keystone_shell_vars.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,151 @@
#!/usr/bin/env python3

# (C) Copyright 2017 Hewlett Packard Enterprise Development LP
#
# 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 base64
import logging
import os
import sys

from typing import Union

from keystoneauth1.identity import Password
from keystoneauth1.session import Session
from keystoneclient.discover import Discover
from requests import HTTPError

from tiny_kubernetes.kubernetes import KubernetesAPIClient, KubernetesAPIResponse

NAMESPACE_FILE = '/var/run/secrets/kubernetes.io/serviceaccount/namespace'

LOG_LEVEL = logging.getLevelName(os.environ.get('LOG_LEVEL', 'INFO'))
logging.basicConfig(level=LOG_LEVEL,
handlers=(logging.StreamHandler(sys.stderr),))

logger = logging.getLogger(__name__)

KEYSTONE_PASSWORD_ARGS = [
'auth_url', 'username', 'password', 'user_id', 'user_domain_id',
'user_domain_name', 'project_id', 'project_name', 'project_domain_id',
'project_domain_name', 'tenant_id', 'tenant_name', 'domain_id',
'domain_name', 'trust_id', 'default_domain_id', 'default_domain_name'
]
KEYSTONE_TIMEOUT = int(os.environ.get('KEYSTONE_TIMEOUT', '10'))
KEYSTONE_VERIFY = os.environ.get('KEYSTONE_VERIFY', 'true') == 'true'
KEYSTONE_CERT = os.environ.get('KEYSTONE_CERT', None)

_kubernetes_client = None


def get_current_namespace() -> str:
if 'NAMESPACE' in os.environ:
return os.environ['NAMESPACE']

if os.path.exists(NAMESPACE_FILE):
with open(NAMESPACE_FILE, 'r') as f:
return f.read()

logger.warning('Not running in cluster and $NAMESPACE is not set, '
'assuming \'default\'!')
return 'default'


def get_kubernetes_client() -> KubernetesAPIClient:
global _kubernetes_client

if _kubernetes_client is None:
_kubernetes_client = KubernetesAPIClient()
_kubernetes_client.load_auto_config()

return _kubernetes_client


def keystone_env_vars():
ret = {}
for arg in KEYSTONE_PASSWORD_ARGS:
name = 'OS_{}'.format(arg.upper())
if name in os.environ:
ret[name] = os.environ[name]

return ret


def keystone_args_from_env():
ret = {}
for arg in KEYSTONE_PASSWORD_ARGS:
ret[arg] = os.environ.get('OS_{}'.format(arg.upper()))

return ret


def get_keystone_client():
auth = Password(**keystone_args_from_env())
session = Session(auth=auth,
app_name='keystone-shell',
user_agent='keystone-shell',
timeout=KEYSTONE_TIMEOUT,
verify=KEYSTONE_VERIFY,
cert=KEYSTONE_CERT)

discover = Discover(session=session)
return discover.create_client()


def get_kubernetes_secret(name: str,
namespace: str=None) -> Union[KubernetesAPIResponse,
None]:
"""
:return: loaded secret dict or None if it does not exist
"""
client = get_kubernetes_client()

if namespace is None:
namespace = get_current_namespace()

try:
return client.get('/api/v1/namespaces/{}/secrets/{}', namespace, name)
except HTTPError as e:
if e.response.status_code != 404:
raise

return None
Copy link
Member

Choose a reason for hiding this comment

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

I think this return should have one less indentation level. I know you run it from except but this way you will make it default.

Copy link
Member Author

Choose a reason for hiding this comment

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

Not that it matters a ton since the code paths are effectively equivalent but my feeling was that (without the conditional to check for a 404) is a bit more clear about the intent.

Ideally it would've been:

try:
    return client.get(...)
except NotFound:
    return None

... which I think makes the intent more clear - that is, None isn't the "default" branch so much as the "not found" branch. Since requests doesn't have exception subclasses for different error codes, it gets a bit uglier when we compare the error code ... I'm not sure how much more readable the end result is, though.



def main():
if len(sys.argv) == 1:
logger.info('no secret name arg provided, will use default environment')
return

secret_name = sys.argv[1]

if len(sys.argv) > 2:
secret_namespace = sys.argv[2]
else:
secret_namespace = None

# purge existing keystone vars from the environment
for var in keystone_env_vars().keys():
print('unset {};'.format(var))

secret = get_kubernetes_secret(secret_name, secret_namespace)
for key, val in secret.data.items():
val_bytes = base64.b64decode(val)
print('export {}="{}"'.format(key, val_bytes.decode('utf-8')))

logger.info('now using account from secret %s', secret_name)


if __name__ == '__main__':
main()
19 changes: 19 additions & 0 deletions keystone-shell/keystonerc.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
#!/bin/sh

secret() {
if [ "$#" -ne 1 ]; then
echo "A secret name is required."
echo "Usage: secret <name>"
return 1
fi

eval "$(python /keystone_shell_vars.py "$1")"
}

shell() {
if [ "$#" -eq 1 ]; then
eval "$(python /keystone_shell_vars.py "$1")"
fi

LD_PRELOAD=/stack-fix.so ptipython -i /ptpython_init.py
}
13 changes: 13 additions & 0 deletions keystone-shell/ptpython_init.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import keystone_shell_vars

keystone = keystone_shell_vars.get_keystone_client()
ks = keystone

kubernetes = keystone_shell_vars.get_kubernetes_client()
k8s = kubernetes

print('--------------------------------------')
print('Pre-defined variables:')
print(' - keystone (ks): keystone client')
print(' - kubernetes (k8s): kubernetes client')
print('--------------------------------------')
7 changes: 7 additions & 0 deletions keystone-shell/requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
dpath
dotmap
pbr>=1.8
keystoneauth1
python-keystoneclient>=3.8.0 # Apache-2.0
requests
-e git+https://github.com/monasca/tiny-kubernetes.git#egg=tiny-kubernetes
30 changes: 30 additions & 0 deletions keystone-shell/stack-fix.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
// workaround for musl's small stack size causing python segfaults
// https://github.com/esnme/ultrajson/issues/254#issuecomment-314862445

#include <dlfcn.h>

#include <pthread.h>

typedef int (*func_t)(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg);

int pthread_create(pthread_t *thread, const pthread_attr_t *attr, void *(*start_routine)(void*), void *arg) {
pthread_attr_t local;
int used = 0, ret;

if (!attr) {
used = 1;
pthread_attr_init(&local);
attr = &local;
}
pthread_attr_setstacksize((void*)attr, 2 * 1024 * 1024); // 2 MB

func_t orig = (func_t)dlsym(RTLD_NEXT, "pthread_create");

ret = orig(thread, attr, start_routine, arg);

if (used) {
pthread_attr_destroy(&local);
}

return ret;
}
Loading