Skip to content
This repository has been archived by the owner on Mar 4, 2024. It is now read-only.

Commit

Permalink
Initial implementation of OpenStack proxy charm
Browse files Browse the repository at this point in the history
  • Loading branch information
johnsca committed Jun 7, 2018
0 parents commit 158777f
Show file tree
Hide file tree
Showing 9 changed files with 369 additions and 0 deletions.
113 changes: 113 additions & 0 deletions README.md
@@ -0,0 +1,113 @@
# Overview

This charm acts as a proxy to OpenStack and provides an [interface][] to apply a
certain set of changes via roles, profiles, and tags to the instances of
the applications that are related to this charm.

## Usage

When on OpenStack, this charm can be deployed, granted trust via Juju to access
OpenStack, and then related to an application that supports the [interface][].

For example, [CDK][] has support for this, and can be deployed with the
following bundle overlay:

```yaml
applications:
openstack:
charm: cs:~containers/openstack
num_units: 1
relations:
- ['openstack', 'kubernetes-master']
- ['openstack', 'kubernetes-worker']
```

Using Juju 2.4-beta1 or later:

```
juju deploy cs:canonical-kubernetes --overlay ./k8s-openstack-overlay.yaml
juju trust openstack
```

To deploy with earlier versions of Juju, you will need to provide the cloud
credentials via the `credentials`, charm config options.

# Examples

Following are some examples using OpenStack integration with CDK.

## Creating a pod with a PersistentDisk-backed volume

This script creates a busybox pod with a persistent volume claim backed by
OpenStack's PersistentDisk.

```sh
#!/bin/bash

# create a storage class using the `kubernetes.io/gce-pd` provisioner
kubectl create -f - <<EOY
apiVersion: storage.k8s.io/v1
kind: StorageClass
metadata:
name: gce-standard
provisioner: kubernetes.io/gce-pd
parameters:
type: pd-standard
EOY

# create a persistent volume claim using that storage class
kubectl create -f - <<EOY
kind: PersistentVolumeClaim
apiVersion: v1
metadata:
name: testclaim
spec:
accessModes:
- ReadWriteOnce
resources:
requests:
storage: 100Mi
storageClassName: gce-standard
EOY

# create the busybox pod with a volume using that PVC:
kubectl create -f - <<EOY
apiVersion: v1
kind: Pod
metadata:
name: busybox
namespace: default
spec:
containers:
- image: busybox
command:
- sleep
- "3600"
imagePullPolicy: IfNotPresent
name: busybox
volumeMounts:
- mountPath: "/pv"
name: testvolume
restartPolicy: Always
volumes:
- name: testvolume
persistentVolumeClaim:
claimName: testclaim
EOY
```

## Creating a service with a OpenStack load-balancer

The following script starts the hello-world pod behind a OpenStack-backed load-balancer.

```sh
#!/bin/bash

kubectl run hello-world --replicas=5 --labels="run=load-balancer-example" --image=gcr.io/google-samples/node-hello:1.0 --port=8080
kubectl expose deployment hello-world --type=LoadBalancer --name=hello
watch kubectl get svc -o wide --selector=run=load-balancer-example
```


[interface]: https://github.com/juju-solutions/interface-openstack
[CDK]: https://jujucharms.com/canonical-kubernetes
15 changes: 15 additions & 0 deletions config.yaml
@@ -0,0 +1,15 @@
options:
credentials:
description: |
The base64-encoded contents of an OpenStack credentials JSON file.
The credentials must contain the following keys: endpoint, username, password,
user-domain-name, project-domain-name, and tenant-name.
This can be used from bundles with 'include-base64://' (see
https://jujucharms.com/docs/stable/charms-bundles#setting-charm-configurations-options-in-a-bundle),
or from the command-line with 'juju config openstack credentials="$(base64 /path/to/file)"'.
It is strongly recommended that you use 'juju trust' instead, if available.
type: string
default: ""
16 changes: 16 additions & 0 deletions copyright
@@ -0,0 +1,16 @@
Format: http://dep.debian.net/deps/dep5/

Files: *
Copyright: Copyright 2018, Canonical Ltd., All Rights Reserved.
License: Apache License 2.0
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.
1 change: 1 addition & 0 deletions icon.svg
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
9 changes: 9 additions & 0 deletions layer.yaml
@@ -0,0 +1,9 @@
repo: 'https://github.com/juju-solutions/charm-openstack'
includes:
- 'layer:basic'
- 'layer:status'
- 'layer:snap'
- 'interface:openstack'
options:
basic:
use_venv: true
150 changes: 150 additions & 0 deletions lib/charms/layer/openstack.py
@@ -0,0 +1,150 @@
import os
import random
import string
import subprocess
from base64 import b64decode

import yaml
from novaclient import client as novaclient_client
from keystoneclient.v3 import client as keystoneclient_v3
from keystoneauth1.session import Session
from keystoneauth1.identity.v3 import Password

from charmhelpers.core import hookenv
from charmhelpers.core.unitdata import kv

from charms.layer import status


# When debugging hooks, for some reason HOME is set to /home/ubuntu, whereas
# during normal hook execution, it's /root. Set it here to be consistent.
os.environ['HOME'] = '/root'


def log(msg, *args):
hookenv.log(msg.format(*args), hookenv.INFO)


def log_err(msg, *args):
hookenv.log(msg.format(*args), hookenv.ERROR)


def get_credentials():
"""
Get the credentials from either the config or the hook tool.
Prefers the config so that it can be overridden.
"""
no_creds_msg = 'missing credentials; set credentials config'
config = hookenv.config()
# try to use Juju's trust feature
try:
result = subprocess.run(['credential-get'],
check=True,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE)
creds = yaml.load(result.stdout.decode('utf8'))
creds_data = creds['credential']['attributes']
_save_creds(creds_data)
_create_project_user()
return True
except FileNotFoundError:
pass # juju trust not available
except subprocess.CalledProcessError as e:
if 'permission denied' not in e.stderr.decode('utf8'):
raise
no_creds_msg = 'missing credentials access; grant with: juju trust'

# try credentials config
if config['credentials']:
try:
creds_data = b64decode(config['credentials']).decode('utf8')
_save_creds(creds_data)
_create_project_user()
return True
except Exception:
status.blocked('invalid value for credentials config')
return False

# no creds provided
status.blocked(no_creds_msg)
return False


def get_user_credentials():
return kv().get('charm.openstack.user-creds')


def cleanup():
_delete_project_user()


# Internal helpers


def _save_creds(creds_data):
kv().set('charm.openstack.full-creds', dict(
auth_url=creds_data['endpoint'],
username=creds_data['username'],
password=creds_data['password'],
user_domain_name=creds_data['user-domain-name'],
project_domain_name=creds_data['project-domain-name'],
project_name=creds_data['tenant-name'],
))


def _load_creds():
return kv().get('charm.openstack.full-creds')


def _get_keystone_client():
auth = Password(**_load_creds())
session = Session(auth=auth, verify=False)
keystone = keystoneclient_v3.Client(session=session)
keystone.auth_ref = auth.get_access(session)
return keystone


def _get_nova_client():
auth = Password(**_load_creds())
session = Session(auth=auth, verify=False)
nova = novaclient_client.Client(2, session=session)
return nova


def _project_user_username():
model_uuid_prefix = os.environ['JUJU_MODEL_UUID'].split('-')[0]
unit_name = hookenv.local_unit().replace('/', '-')
return 'juju-charm-{}-{}'.format(model_uuid_prefix, unit_name)


def _create_project_user():
"""
Create a (slightly) more limited user in the project.
"""
client = _get_keystone_client()
alphabet = string.ascii_letters + string.digits
full_creds = _load_creds()
username = _project_user_username()
password = ''.join(random.choice(alphabet) for _ in range(20))
client.users.create(
name=username,
password=password,
domain=full_creds['project_domain_name'],
default_project=full_creds['project_name'],
)
kv().set('charm.openstack.user-creds', dict(
auth_url=full_creds['endpoint'],
username=username,
password=password,
user_domain_name=full_creds['user_domain_name'],
project_domain_name=full_creds['project_domain_name'],
project_name=full_creds['tenant_name'],
))


def _delete_project_user():
client = _get_keystone_client()
username = _project_user_username()
client.users.delete(username)
kv().unset('charm.openstack.user-creds')
15 changes: 15 additions & 0 deletions metadata.yaml
@@ -0,0 +1,15 @@
name: openstack
display-name: OpenStack
summary: |
Proxy charm to enable OpenStack integrations via Juju relations.
description: |
This charm can grant select permissions to instances of applications
related to it which enable integration with OpenStack specific features,
such as firewalls, load balancing, block storage, object storage, etc.
maintainers: ['Cory Johns <johnsca@gmail.com>']
series:
- xenial
tags: ['openstack', 'native', 'integration']
provides:
openstack:
interface: openstack
48 changes: 48 additions & 0 deletions reactive/openstack.py
@@ -0,0 +1,48 @@
from charms.reactive import (
hook,
when_all,
when_any,
when_not,
toggle_flag,
clear_flag,
)
from charms.reactive.relations import endpoint_from_name

from charms import layer


@when_any('config.changed.credentials')
def update_creds():
clear_flag('charm.openstack.creds.set')


@when_not('charm.openstack.creds.set')
def get_creds():
toggle_flag('charm.openstack.creds.set', layer.openstack.get_credentials())


@when_all('charm.openstack.creds.set')
@when_not('endpoint.openstack.requests-pending')
def no_requests():
openstack = endpoint_from_name('openstack')
layer.openstack.cleanup(openstack.relation_ids)
layer.status.active('ready')


@when_all('charm.openstack.creds.set',
'endpoint.openstack.requests-pending')
def handle_requests():
openstack = endpoint_from_name('openstack')
for request in openstack.requests:
layer.status.maintenance(
'granting request for {}'.format(request.unit_name))
if not request.has_credentials:
creds = layer.openstack.get_user_credentials()
request.set_credentials(creds)
layer.openstack.log('Finished request for {}', request.unit_name)
openstack.mark_completed()


@hook('stop')
def cleanup():
layer.openstack.cleanup()
2 changes: 2 additions & 0 deletions wheelhouse.txt
@@ -0,0 +1,2 @@
python-keystoneclient
python-novaclient

0 comments on commit 158777f

Please sign in to comment.