~lazypower/charms/trusty/keystone/fix_proof

« back to all changes in this revision

Viewing changes to hooks/keystone_utils.py

  • Committer: Ante Karamatic
  • Date: 2014-02-25 11:34:13 UTC
  • Revision ID: ivoks@ubuntu.com-20140225113413-tlm02x1ibc6xb10d
Rewrite charm to get it more in line with other OpenStack charms.

Added support for contexts and templating. Makes use of charm-helpers
instead of relaying on its own tools (probably could use some additional work).

HA is currently non-functional. ETA for fixing: less than 2 days.

Show diffs side-by-side

added added

removed removed

Lines of Context:
1
1
#!/usr/bin/python
2
 
import ConfigParser
3
 
import sys
4
 
import json
5
 
import time
6
2
import subprocess
7
3
import os
8
 
 
9
 
from lib.openstack_common import(
 
4
import urlparse
 
5
 
 
6
from base64 import b64encode
 
7
from collections import OrderedDict
 
8
from copy import deepcopy
 
9
 
 
10
from charmhelpers.contrib.hahelpers.cluster import(
 
11
    eligible_leader,
 
12
    determine_api_port,
 
13
    https,
 
14
    is_clustered
 
15
    )
 
16
 
 
17
from charmhelpers.contrib.openstack import context, templating
 
18
 
 
19
from charmhelpers.contrib.openstack.utils import (
 
20
    configure_installation_source,
 
21
    error_out,
10
22
    get_os_codename_install_source,
11
 
    get_os_codename_package,
12
 
    error_out,
13
 
    configure_installation_source
14
 
    )
15
 
 
 
23
    os_release,
 
24
    save_script_rc as _save_script_rc)
 
25
 
 
26
import charmhelpers.contrib.unison as unison
 
27
 
 
28
from charmhelpers.core.hookenv import (
 
29
    config,
 
30
    log,
 
31
    relation_get,
 
32
    relation_set,
 
33
    unit_private_ip,
 
34
    INFO,
 
35
)
 
36
 
 
37
from charmhelpers.fetch import (
 
38
    apt_install,
 
39
    apt_update,
 
40
)
 
41
 
 
42
import keystone_context
16
43
import keystone_ssl as ssl
17
 
import lib.unison as unison
18
 
import lib.utils as utils
19
 
import lib.cluster_utils as cluster
20
 
 
21
 
 
22
 
keystone_conf = "/etc/keystone/keystone.conf"
23
 
stored_passwd = "/var/lib/keystone/keystone.passwd"
24
 
stored_token = "/var/lib/keystone/keystone.token"
 
44
 
 
45
TEMPLATES = 'templates/'
 
46
 
 
47
# removed from original: charm-helper-sh
 
48
BASE_PACKAGES = [
 
49
    'apache2',
 
50
    'haproxy',
 
51
    'openssl',
 
52
    'python-keystoneclient',
 
53
    'python-mysqldb',
 
54
    'pwgen',
 
55
    'unison',
 
56
    'uuid',
 
57
]
 
58
 
 
59
BASE_SERVICES = [
 
60
    'keystone',
 
61
]
 
62
 
 
63
API_PORTS = {
 
64
    'keystone-admin': config('admin-port'),
 
65
    'keystone-public': config('service-port')
 
66
}
 
67
 
 
68
KEYSTONE_CONF = "/etc/keystone/keystone.conf"
 
69
STORED_PASSWD = "/var/lib/keystone/keystone.passwd"
 
70
STORED_TOKEN = "/var/lib/keystone/keystone.token"
25
71
SERVICE_PASSWD_PATH = '/var/lib/keystone/services.passwd'
26
72
 
 
73
HAPROXY_CONF = '/etc/haproxy/haproxy.cfg'
 
74
APACHE_CONF = '/etc/apache2/sites-available/openstack_https_frontend'
 
75
APACHE_24_CONF = '/etc/apache2/sites-available/openstack_https_frontend.conf'
 
76
 
27
77
SSL_DIR = '/var/lib/keystone/juju_ssl/'
28
78
SSL_CA_NAME = 'Ubuntu Cloud'
29
79
CLUSTER_RES = 'res_ks_vip'
30
80
SSH_USER = 'juju_keystone'
31
81
 
32
 
 
33
 
def execute(cmd, die=False, echo=False):
34
 
    """ Executes a command
35
 
 
36
 
    if die=True, script will exit(1) if command does not return 0
37
 
    if echo=True, output of command will be printed to stdout
38
 
 
39
 
    returns a tuple: (stdout, stderr, return code)
40
 
    """
41
 
    p = subprocess.Popen(cmd.split(" "),
42
 
                         stdout=subprocess.PIPE,
43
 
                         stdin=subprocess.PIPE,
44
 
                         stderr=subprocess.PIPE)
45
 
    stdout = ""
46
 
    stderr = ""
47
 
 
48
 
    def print_line(l):
49
 
        if echo:
50
 
            print l.strip('\n')
51
 
            sys.stdout.flush()
52
 
 
53
 
    for l in iter(p.stdout.readline, ''):
54
 
        print_line(l)
55
 
        stdout += l
56
 
    for l in iter(p.stderr.readline, ''):
57
 
        print_line(l)
58
 
        stderr += l
59
 
 
60
 
    p.communicate()
61
 
    rc = p.returncode
62
 
 
63
 
    if die and rc != 0:
64
 
        error_out("ERROR: command %s return non-zero.\n" % cmd)
65
 
    return (stdout, stderr, rc)
66
 
 
67
 
 
68
 
def config_get():
69
 
    """ Obtain the units config via 'config-get'
70
 
    Returns a dict representing current config.
71
 
    private-address and IP of the unit is also tacked on for
72
 
    convienence
73
 
    """
74
 
    output = execute("config-get --format json")[0]
75
 
    config = json.loads(output)
76
 
    # make sure no config element is blank after config-get
77
 
    for c in config.keys():
78
 
        if config[c] is None:
79
 
            error_out("ERROR: Config option has no paramter: %s" % c)
80
 
    # tack on our private address and ip
81
 
    config["hostname"] = utils.unit_get('private-address')
82
 
    return config
83
 
 
84
 
 
85
 
@utils.cached
 
82
BASE_RESOURCE_MAP = OrderedDict([
 
83
    (KEYSTONE_CONF, {
 
84
        'services': BASE_SERVICES,
 
85
        'contexts': [keystone_context.KeystoneContext(),
 
86
                     context.SharedDBContext(),
 
87
                     context.SyslogContext(),
 
88
                     keystone_context.HAProxyContext()],
 
89
    }),
 
90
    (HAPROXY_CONF, {
 
91
        'contexts': [context.HAProxyContext(),
 
92
                     keystone_context.HAProxyContext()],
 
93
        'services': ['haproxy'],
 
94
    }),
 
95
    (APACHE_CONF, {
 
96
        'contexts': [keystone_context.ApacheSSLContext()],
 
97
        'services': ['apache2'],
 
98
    }),
 
99
    (APACHE_24_CONF, {
 
100
        'contexts': [keystone_context.ApacheSSLContext()],
 
101
        'services': ['apache2'],
 
102
    }),
 
103
])
 
104
 
 
105
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
 
106
 
 
107
valid_services = {
 
108
    "nova": {
 
109
        "type": "compute",
 
110
        "desc": "Nova Compute Service"
 
111
    },
 
112
    "nova-volume": {
 
113
        "type": "volume",
 
114
        "desc": "Nova Volume Service"
 
115
    },
 
116
    "cinder": {
 
117
        "type": "volume",
 
118
        "desc": "Cinder Volume Service"
 
119
    },
 
120
    "ec2": {
 
121
        "type": "ec2",
 
122
        "desc": "EC2 Compatibility Layer"
 
123
    },
 
124
    "glance": {
 
125
        "type": "image",
 
126
        "desc": "Glance Image Service"
 
127
    },
 
128
    "s3": {
 
129
        "type": "s3",
 
130
        "desc": "S3 Compatible object-store"
 
131
    },
 
132
    "swift": {
 
133
        "type": "object-store",
 
134
        "desc": "Swift Object Storage Service"
 
135
    },
 
136
    "quantum": {
 
137
        "type": "network",
 
138
        "desc": "Quantum Networking Service"
 
139
    },
 
140
    "oxygen": {
 
141
        "type": "oxygen",
 
142
        "desc": "Oxygen Cloud Image Service"
 
143
    },
 
144
    "ceilometer": {
 
145
        "type": "metering",
 
146
        "desc": "Ceilometer Metering Service"
 
147
    },
 
148
    "heat": {
 
149
        "type": "orchestration",
 
150
        "desc": "Heat Orchestration API"
 
151
    },
 
152
    "heat-cfn": {
 
153
        "type": "cloudformation",
 
154
        "desc": "Heat CloudFormation API"
 
155
    }
 
156
}
 
157
 
 
158
def resource_map():
 
159
    '''
 
160
    Dynamically generate a map of resources that will be managed for a single
 
161
    hook execution.
 
162
    '''
 
163
    resource_map = deepcopy(BASE_RESOURCE_MAP)
 
164
 
 
165
    if os.path.exists('/etc/apache2/conf-available'):
 
166
        resource_map.pop(APACHE_CONF)
 
167
    else:
 
168
        resource_map.pop(APACHE_24_CONF)
 
169
    return resource_map
 
170
 
 
171
 
 
172
def register_configs():
 
173
    release = os_release('keystone')
 
174
    configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
 
175
                                          openstack_release=release)
 
176
    for cfg, rscs in resource_map().iteritems():
 
177
        configs.register(cfg, rscs['contexts'])
 
178
    return configs
 
179
 
 
180
 
 
181
def restart_map():
 
182
    return OrderedDict([(cfg, v['services'])
 
183
                        for cfg, v in resource_map().iteritems()
 
184
                        if v['services']])
 
185
 
 
186
 
 
187
def determine_ports():
 
188
    '''Assemble a list of API ports for services we are managing'''
 
189
    ports = [config('admin-port'), config('service-port')]
 
190
    return list(set(ports))
 
191
 
 
192
 
 
193
def api_port(service):
 
194
    return API_PORTS[service]
 
195
 
 
196
 
 
197
def determine_packages():
 
198
    # currently all packages match service names
 
199
    packages = [] + BASE_PACKAGES
 
200
    for k, v in resource_map().iteritems():
 
201
        packages.extend(v['services'])
 
202
    return list(set(packages))
 
203
 
 
204
 
 
205
def save_script_rc():
 
206
    env_vars = {'OPENSTACK_SERVICE_KEYSTONE': 'keystone',
 
207
                'OPENSTACK_PORT_ADMIN': determine_api_port(
 
208
                    api_port('keystone-admin')),
 
209
                'OPENSTACK_PORT_PUBLIC': determine_api_port(
 
210
                    api_port('keystone-public'))}
 
211
    _save_script_rc(**env_vars)
 
212
 
 
213
 
 
214
def do_openstack_upgrade(configs):
 
215
    new_src = config('openstack-origin')
 
216
    new_os_rel = get_os_codename_install_source(new_src)
 
217
    log('Performing OpenStack upgrade to %s.' % (new_os_rel))
 
218
 
 
219
    configure_installation_source(new_src)
 
220
    apt_update()
 
221
 
 
222
    dpkg_opts = [
 
223
        '--option', 'Dpkg::Options::=--force-confnew',
 
224
        '--option', 'Dpkg::Options::=--force-confdef',
 
225
    ]
 
226
 
 
227
    apt_install(packages=determine_packages(), options=dpkg_opts, fatal=True)
 
228
 
 
229
    # set CONFIGS to load templates from new release and regenerate config
 
230
    configs.set_release(openstack_release=new_os_rel)
 
231
    configs.write_all()
 
232
 
 
233
    if eligible_leader(CLUSTER_RES):
 
234
        migrate_database()
 
235
 
 
236
def migrate_database():
 
237
    '''Runs keystone-manage to initialize a new database or migrate existing'''
 
238
    log('Migrating the keystone database.', level=INFO)
 
239
    cmd = ['keystone-manage', 'db_sync']
 
240
    subprocess.check_output(cmd)
 
241
 
 
242
 
 
243
## OLD
 
244
 
86
245
def get_local_endpoint():
87
246
    """ Returns the URL for the local end-point bypassing haproxy/ssl """
88
247
    local_endpoint = 'http://localhost:{}/v2.0/'.format(
89
 
        cluster.determine_api_port(utils.config_get('admin-port'))
 
248
        determine_api_port(api_port('keystone-admin'))
90
249
        )
91
250
    return local_endpoint
92
251
 
93
252
 
94
 
def set_admin_token(admin_token):
 
253
def set_admin_token(admin_token='None'):
95
254
    """Set admin token according to deployment config or use a randomly
96
255
       generated token if none is specified (default).
97
256
    """
98
257
    if admin_token != 'None':
99
 
        utils.juju_log('INFO',
100
 
                       'Configuring Keystone to use'
101
 
                       ' a pre-configured admin token.')
 
258
        log('Configuring Keystone to use a pre-configured admin token.')
102
259
        token = admin_token
103
260
    else:
104
 
        utils.juju_log('INFO',
105
 
                       'Configuring Keystone to use a random admin token.')
106
 
        if os.path.isfile(stored_token):
 
261
        log('Configuring Keystone to use a random admin token.')
 
262
        if os.path.isfile(STORED_TOKEN):
107
263
            msg = 'Loading a previously generated' \
108
 
                  ' admin token from %s' % stored_token
109
 
            utils.juju_log('INFO', msg)
110
 
            f = open(stored_token, 'r')
 
264
                  ' admin token from %s' % STORED_TOKEN
 
265
            log(msg)
 
266
            f = open(STORED_TOKEN, 'r')
111
267
            token = f.read().strip()
112
268
            f.close()
113
269
        else:
114
 
            token = execute('pwgen -c 32 1', die=True)[0].strip()
115
 
            out = open(stored_token, 'w')
 
270
            cmd = ['pwgen', '-c', '32', '1']
 
271
            token = str(subprocess.check_output(cmd)).strip()
 
272
            out = open(STORED_TOKEN, 'w')
116
273
            out.write('%s\n' % token)
117
274
            out.close()
118
 
    update_config_block('DEFAULT', admin_token=token)
 
275
    return(token)
119
276
 
120
277
 
121
278
def get_admin_token():
122
279
    """Temporary utility to grab the admin token as configured in
123
280
       keystone.conf
124
281
    """
125
 
    with open(keystone_conf, 'r') as f:
 
282
    with open(KEYSTONE_CONF, 'r') as f:
126
283
        for l in f.readlines():
127
284
            if l.split(' ')[0] == 'admin_token':
128
285
                try:
129
286
                    return l.split('=')[1].strip()
130
287
                except:
131
288
                    error_out('Could not parse admin_token line from %s' %
132
 
                              keystone_conf)
133
 
    error_out('Could not find admin_token line in %s' % keystone_conf)
134
 
 
135
 
 
136
 
# Track all updated config settings.
137
 
_config_dirty = [False]
138
 
 
139
 
def config_dirty():
140
 
    return True in _config_dirty
141
 
 
142
 
def update_config_block(section, **kwargs):
143
 
    """ Updates keystone.conf blocks given kwargs.
144
 
    Update a config setting in a specific setting of a config
145
 
    file (/etc/keystone/keystone.conf, by default)
146
 
    """
147
 
    if 'file' in kwargs:
148
 
        conf_file = kwargs['file']
149
 
        del kwargs['file']
150
 
    else:
151
 
        conf_file = keystone_conf
152
 
    config = ConfigParser.RawConfigParser()
153
 
    config.read(conf_file)
154
 
 
155
 
    if section != 'DEFAULT' and not config.has_section(section):
156
 
        config.add_section(section)
157
 
        _config_dirty[0] = True
158
 
 
159
 
    for k, v in kwargs.iteritems():
160
 
        try:
161
 
            cur = config.get(section, k)
162
 
            if cur != v:
163
 
                _config_dirty[0] = True
164
 
        except (ConfigParser.NoSectionError,
165
 
                ConfigParser.NoOptionError):
166
 
            _config_dirty[0] = True
167
 
        config.set(section, k, v)
168
 
    with open(conf_file, 'wb') as out:
169
 
        config.write(out)
 
289
                              KEYSTONE_CONF)
 
290
    error_out('Could not find admin_token line in %s' % KEYSTONE_CONF)
170
291
 
171
292
 
172
293
def create_service_entry(service_name, service_type, service_desc, owner=None):
176
297
                                      token=get_admin_token())
177
298
    for service in [s._info for s in manager.api.services.list()]:
178
299
        if service['name'] == service_name:
179
 
            utils.juju_log('INFO',
180
 
                           "Service entry for '%s' already exists." % \
181
 
                           service_name)
 
300
            log("Service entry for '%s' already exists." % service_name)
182
301
            return
183
302
    manager.api.services.create(name=service_name,
184
303
                                service_type=service_type,
185
304
                                description=service_desc)
186
 
    utils.juju_log('INFO', "Created new service entry '%s'" % service_name)
 
305
    log("Created new service entry '%s'" % service_name)
187
306
 
188
307
 
189
308
def create_endpoint_template(region, service,  publicurl, adminurl,
196
315
    service_id = manager.resolve_service_id(service)
197
316
    for ep in [e._info for e in manager.api.endpoints.list()]:
198
317
        if ep['service_id'] == service_id and ep['region'] == region:
199
 
            utils.juju_log('INFO',
200
 
                           "Endpoint template already exists for '%s' in '%s'"
201
 
                           % (service, region))
 
318
            log("Endpoint template already exists for '%s' in '%s'"
 
319
                  % (service, region))
202
320
 
203
321
            up_to_date = True
204
322
            for k in ['publicurl', 'adminurl', 'internalurl']:
209
327
                return
210
328
            else:
211
329
                # delete endpoint and recreate if endpoint urls need updating.
212
 
                utils.juju_log('INFO',
213
 
                               "Updating endpoint template with"
214
 
                               " new endpoint urls.")
 
330
                log("Updating endpoint template with new endpoint urls.")
215
331
                manager.api.endpoints.delete(ep['id'])
216
332
 
217
333
    manager.api.endpoints.create(region=region,
219
335
                                 publicurl=publicurl,
220
336
                                 adminurl=adminurl,
221
337
                                 internalurl=internalurl)
222
 
    utils.juju_log('INFO', "Created new endpoint template for '%s' in '%s'" %
223
 
                   (region, service))
 
338
    log("Created new endpoint template for '%s' in '%s'" % (region, service))
224
339
 
225
340
 
226
341
def create_tenant(name):
232
347
    if not tenants or name not in [t['name'] for t in tenants]:
233
348
        manager.api.tenants.create(tenant_name=name,
234
349
                                   description='Created by Juju')
235
 
        utils.juju_log('INFO', "Created new tenant: %s" % name)
 
350
        log("Created new tenant: %s" % name)
236
351
        return
237
 
    utils.juju_log('INFO', "Tenant '%s' already exists." % name)
 
352
    log("Tenant '%s' already exists." % name)
238
353
 
239
354
 
240
355
def create_user(name, password, tenant):
251
366
                                 password=password,
252
367
                                 email='juju@localhost',
253
368
                                 tenant_id=tenant_id)
254
 
        utils.juju_log('INFO', "Created new user '%s' tenant: %s" % \
255
 
                       (name, tenant_id))
 
369
        log("Created new user '%s' tenant: %s" % (name, tenant_id))
256
370
        return
257
 
    utils.juju_log('INFO', "A user named '%s' already exists" % name)
 
371
    log("A user named '%s' already exists" % name)
258
372
 
259
373
 
260
374
def create_role(name, user=None, tenant=None):
265
379
    roles = [r._info for r in manager.api.roles.list()]
266
380
    if not roles or name not in [r['name'] for r in roles]:
267
381
        manager.api.roles.create(name=name)
268
 
        utils.juju_log('INFO', "Created new role '%s'" % name)
 
382
        log("Created new role '%s'" % name)
269
383
    else:
270
 
        utils.juju_log('INFO', "A role named '%s' already exists" % name)
 
384
        log("A role named '%s' already exists" % name)
271
385
 
272
386
    if not user and not tenant:
273
387
        return
289
403
    import manager
290
404
    manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
291
405
                                      token=get_admin_token())
292
 
    utils.juju_log('INFO', "Granting user '%s' role '%s' on tenant '%s'" % \
293
 
                   (user, role, tenant))
 
406
    log("Granting user '%s' role '%s' on tenant '%s'" % \
 
407
       (user, role, tenant))
294
408
    user_id = manager.resolve_user_id(user)
295
409
    role_id = manager.resolve_role_id(role)
296
410
    tenant_id = manager.resolve_tenant_id(tenant)
300
414
        manager.api.roles.add_user_role(user=user_id,
301
415
                                        role=role_id,
302
416
                                        tenant=tenant_id)
303
 
        utils.juju_log('INFO', "Granted user '%s' role '%s' on tenant '%s'" % \
304
 
                       (user, role, tenant))
305
 
    else:
306
 
        utils.juju_log('INFO',
307
 
                       "User '%s' already has role '%s' on tenant '%s'" % \
308
 
                       (user, role, tenant))
309
 
 
310
 
 
311
 
def generate_admin_token(config):
312
 
    """ generate and add an admin token """
313
 
    import manager
314
 
    manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
315
 
                                      token='ADMIN')
316
 
    if config["admin-token"] == "None":
317
 
        import random
318
 
        token = random.randrange(1000000000000, 9999999999999)
319
 
    else:
320
 
        return config["admin-token"]
321
 
    manager.api.add_token(token, config["admin-user"],
322
 
                          "admin", config["token-expiry"])
323
 
    utils.juju_log('INFO', "Generated and added new random admin token.")
324
 
    return token
 
417
        log("Granted user '%s' role '%s' on tenant '%s'" % \
 
418
           (user, role, tenant))
 
419
    else:
 
420
        log("User '%s' already has role '%s' on tenant '%s'" % \
 
421
           (user, role, tenant))
325
422
 
326
423
 
327
424
def ensure_initial_admin(config):
335
432
        changes?
336
433
    """
337
434
    create_tenant("admin")
338
 
    create_tenant(config["service-tenant"])
 
435
    create_tenant(config("service-tenant"))
339
436
 
340
437
    passwd = ""
341
 
    if config["admin-password"] != "None":
342
 
        passwd = config["admin-password"]
343
 
    elif os.path.isfile(stored_passwd):
344
 
        utils.juju_log('INFO', "Loading stored passwd from %s" % stored_passwd)
345
 
        passwd = open(stored_passwd, 'r').readline().strip('\n')
 
438
    if config("admin-password") != "None":
 
439
        passwd = config("admin-password")
 
440
    elif os.path.isfile(STORED_PASSWD):
 
441
        log("Loading stored passwd from %s" % STORED_PASSWD)
 
442
        passwd = open(STORED_PASSWD, 'r').readline().strip('\n')
346
443
    if passwd == "":
347
 
        utils.juju_log('INFO', "Generating new passwd for user: %s" % \
348
 
                       config["admin-user"])
349
 
        passwd = execute("pwgen -c 16 1", die=True)[0]
350
 
        open(stored_passwd, 'w+').writelines("%s\n" % passwd)
 
444
        log("Generating new passwd for user: %s" % \
 
445
                  config("admin-user"))
 
446
        cmd = ['pwgen', '-c', '16', '1']
 
447
        passwd = str(subprocess.check_output(cmd)).strip()
 
448
        open(STORED_PASSWD, 'w+').writelines("%s\n" % passwd)
351
449
 
352
 
    create_user(config['admin-user'], passwd, tenant='admin')
353
 
    update_user_password(config['admin-user'], passwd)
354
 
    create_role(config['admin-role'], config['admin-user'], 'admin')
 
450
    create_user(config('admin-user'), passwd, tenant='admin')
 
451
    update_user_password(config('admin-user'), passwd)
 
452
    create_role(config('admin-role'), config('admin-user'), 'admin')
355
453
    # TODO(adam_g): The following roles are likely not needed since redux merge
356
 
    create_role("KeystoneAdmin", config["admin-user"], 'admin')
357
 
    create_role("KeystoneServiceAdmin", config["admin-user"], 'admin')
 
454
    create_role("KeystoneAdmin", config("admin-user"), 'admin')
 
455
    create_role("KeystoneServiceAdmin", config("admin-user"), 'admin')
358
456
    create_service_entry("keystone", "identity", "Keystone Identity Service")
359
457
 
360
 
    if cluster.is_clustered():
361
 
        utils.juju_log('INFO', "Creating endpoint for clustered configuration")
362
 
        service_host = auth_host = config["vip"]
 
458
    if is_clustered():
 
459
        log("Creating endpoint for clustered configuration")
 
460
        service_host = auth_host = config("vip")
363
461
    else:
364
 
        utils.juju_log('INFO', "Creating standard endpoint")
365
 
        service_host = auth_host = config["hostname"]
 
462
        log("Creating standard endpoint")
 
463
        service_host = auth_host = unit_private_ip()
366
464
 
367
 
    for region in config['region'].split():
 
465
    for region in config('region').split():
368
466
        create_keystone_endpoint(service_host=service_host,
369
 
                                 service_port=config["service-port"],
 
467
                                 service_port=config("service-port"),
370
468
                                 auth_host=auth_host,
371
 
                                 auth_port=config["admin-port"],
 
469
                                 auth_port=config("admin-port"),
372
470
                                 region=region)
373
471
 
374
472
 
375
473
def create_keystone_endpoint(service_host, service_port,
376
474
                             auth_host, auth_port, region):
377
 
    public_url = "http://%s:%s/v2.0" % (service_host, service_port)
378
 
    admin_url = "http://%s:%s/v2.0" % (auth_host, auth_port)
379
 
    internal_url = "http://%s:%s/v2.0" % (service_host, service_port)
 
475
    proto = 'http'
 
476
    if https():
 
477
        log("Setting https keystone endpoint")
 
478
        proto = 'https'
 
479
    public_url = "%s://%s:%s/v2.0" % (proto, service_host, service_port)
 
480
    admin_url = "%s://%s:%s/v2.0" % (proto, auth_host, auth_port)
 
481
    internal_url = "%s://%s:%s/v2.0" % (proto, service_host, service_port)
380
482
    create_endpoint_template(region, "keystone", public_url,
381
483
                             admin_url, internal_url)
382
484
 
385
487
    import manager
386
488
    manager = manager.KeystoneManager(endpoint=get_local_endpoint(),
387
489
                                      token=get_admin_token())
388
 
    utils.juju_log('INFO', "Updating password for user '%s'" % username)
 
490
    log("Updating password for user '%s'" % username)
389
491
 
390
492
    user_id = manager.resolve_user_id(username)
391
493
    if user_id is None:
392
494
        error_out("Could not resolve user id for '%s'" % username)
393
495
 
394
496
    manager.api.users.update_password(user=user_id, password=password)
395
 
    utils.juju_log('INFO', "Successfully updated password for user '%s'" % \
396
 
                   username)
 
497
    log("Successfully updated password for user '%s'" % \
 
498
              username)
397
499
 
398
500
 
399
501
def load_stored_passwords(path=SERVICE_PASSWD_PATH):
425
527
    return passwd
426
528
 
427
529
 
428
 
def configure_pki_tokens(config):
429
 
    '''Configure PKI token signing, if enabled.'''
430
 
    if config['enable-pki'] not in ['True', 'true']:
431
 
        update_config_block('signing', token_format='UUID')
432
 
    else:
433
 
        utils.juju_log('INFO', 'TODO: PKI Support, setting to UUID for now.')
434
 
        update_config_block('signing', token_format='UUID')
435
 
 
436
 
 
437
 
def do_openstack_upgrade(install_src, packages):
438
 
    '''Upgrade packages from a given install src.'''
439
 
 
440
 
    config = config_get()
441
 
    old_vers = get_os_codename_package('keystone')
442
 
    new_vers = get_os_codename_install_source(install_src)
443
 
 
444
 
    utils.juju_log('INFO',
445
 
                   "Beginning Keystone upgrade: %s -> %s" % \
446
 
                   (old_vers, new_vers))
447
 
 
448
 
    # Backup previous config.
449
 
    utils.juju_log('INFO', "Backing up contents of /etc/keystone.")
450
 
    stamp = time.strftime('%Y%m%d%H%M')
451
 
    cmd = 'tar -pcf /var/lib/juju/keystone-backup-%s.tar /etc/keystone' % stamp
452
 
    execute(cmd, die=True, echo=True)
453
 
 
454
 
    configure_installation_source(install_src)
455
 
    execute('apt-get update', die=True, echo=True)
456
 
    os.environ['DEBIAN_FRONTEND'] = 'noninteractive'
457
 
    cmd = 'apt-get --option Dpkg::Options::=--force-confnew -y '\
458
 
          'install %s' % packages
459
 
    execute(cmd, echo=True, die=True)
460
 
 
461
 
    # we have new, fresh config files that need updating.
462
 
    # set the admin token, which is still stored in config.
463
 
    set_admin_token(config['admin-token'])
464
 
 
465
 
    # set the sql connection string if a shared-db relation is found.
466
 
    ids = utils.relation_ids('shared-db')
467
 
 
468
 
    if ids:
469
 
        for rid in ids:
470
 
            for unit in utils.relation_list(rid):
471
 
                utils.juju_log('INFO',
472
 
                               'Configuring new keystone.conf for '
473
 
                               'database access on existing database'
474
 
                               ' relation to %s' % unit)
475
 
                relation_data = utils.relation_get_dict(relation_id=rid,
476
 
                                                        remote_unit=unit)
477
 
 
478
 
                update_config_block('sql', connection="mysql://%s:%s@%s/%s" %
479
 
                                        (config["database-user"],
480
 
                                         relation_data["password"],
481
 
                                         relation_data["private-address"],
482
 
                                         config["database"]))
483
 
 
484
 
    utils.stop('keystone')
485
 
    if (cluster.eligible_leader(CLUSTER_RES)):
486
 
        utils.juju_log('INFO',
487
 
                       'Running database migrations for %s' % new_vers)
488
 
        execute('keystone-manage db_sync', echo=True, die=True)
489
 
    else:
490
 
        utils.juju_log('INFO',
491
 
                       'Not cluster leader; snoozing whilst'
492
 
                       ' leader upgrades DB')
493
 
        time.sleep(10)
494
 
    utils.start('keystone')
495
 
    time.sleep(5)
496
 
    utils.juju_log('INFO',
497
 
                   'Completed Keystone upgrade: '
498
 
                   '%s -> %s' % (old_vers, new_vers))
499
 
 
500
 
 
501
530
def synchronize_service_credentials():
502
531
    '''
503
532
    Broadcast service credentials to peers or consume those that have been
504
533
    broadcasted by peer, depending on hook context.
505
534
    '''
506
 
    if (not cluster.eligible_leader(CLUSTER_RES) or
 
535
    if (not eligible_leader(CLUSTER_RES) or
507
536
        not os.path.isfile(SERVICE_PASSWD_PATH)):
508
537
        return
509
 
    utils.juju_log('INFO', 'Synchronizing service passwords to all peers.')
510
 
    unison.sync_to_peers(peer_interface='cluster',
511
 
                         paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
512
 
                         verbose=True)
 
538
    log('Synchronizing service passwords to all peers.')
 
539
    if is_clustered():
 
540
        unison.sync_to_peers(peer_interface='cluster',
 
541
                             paths=[SERVICE_PASSWD_PATH], user=SSH_USER,
 
542
                             verbose=True)
513
543
 
514
544
CA = []
515
545
 
530
560
                                            '%s_root_ca' % d_name))
531
561
        # SSL_DIR is synchronized via all peers over unison+ssh, need
532
562
        # to ensure permissions.
533
 
        execute('chown -R %s.%s %s' % (user, group, SSL_DIR))
534
 
        execute('chmod -R g+rwx %s' % SSL_DIR)
 
563
        subprocess.check_output(['chown', '-R', '%s.%s' % (user, group),
 
564
                                '%s' % SSL_DIR])
 
565
        subprocess.check_output(['chmod', '-R', 'g+rwx', '%s' % SSL_DIR])
535
566
        CA.append(ca)
536
567
    return CA[0]
537
568
 
538
569
 
539
 
def https():
540
 
    if (utils.config_get('https-service-endpoints') in ["yes", "true", "True"]
541
 
        or cluster.https()):
542
 
        return True
543
 
    else:
544
 
        return False
 
570
def relation_list(rid):
 
571
    cmd = [
 
572
        'relation-list',
 
573
        '-r', rid,
 
574
        ]
 
575
    result = str(subprocess.check_output(cmd)).split()
 
576
    if result == "":
 
577
        return None
 
578
    else:
 
579
        return result
 
580
 
 
581
def add_service_to_keystone():
 
582
    settings = relation_get()
 
583
    # the minimum settings needed per endpoint
 
584
    single = set(['service', 'region', 'public_url', 'admin_url',
 
585
                  'internal_url'])
 
586
    if single.issubset(settings):
 
587
        # other end of relation advertised only one endpoint
 
588
        if 'None' in [v for k, v in settings.iteritems()]:
 
589
            # Some backend services advertise no endpoint but require a
 
590
            # hook execution to update auth strategy.
 
591
            relation_data = {}
 
592
            # Check if clustered and use vip + haproxy ports if so
 
593
            if is_clustered():
 
594
                relation_data["auth_host"] = config('vip')
 
595
                relation_data["service_host"] = config('vip')
 
596
            else:
 
597
                relation_data["auth_host"] = config('hostname')
 
598
                relation_data["service_host"] = config('hostname')
 
599
            relation_data["auth_port"] = config('admin-port')
 
600
            relation_data["service_port"] = config('service-port')
 
601
            if config('https-service-endpoints') in ['True', 'true']:
 
602
                # Pass CA cert as client will need it to
 
603
                # verify https connections
 
604
                ca = get_ca(user=SSH_USER)
 
605
                ca_bundle = ca.get_ca_bundle()
 
606
                relation_data['https_keystone'] = 'True'
 
607
                relation_data['ca_cert'] = b64encode(ca_bundle)
 
608
            # Allow the remote service to request creation of any additional
 
609
            # roles. Currently used by Horizon
 
610
            for role in get_requested_roles(settings):
 
611
                log("Creating requested role: %s" % role)
 
612
                create_role(role)
 
613
            relation_set(**relation_data)
 
614
            return
 
615
        else:
 
616
            ensure_valid_service(settings['service'])
 
617
            add_endpoint(region=settings['region'],
 
618
                         service=settings['service'],
 
619
                         publicurl=settings['public_url'],
 
620
                         adminurl=settings['admin_url'],
 
621
                         internalurl=settings['internal_url'])
 
622
            service_username = settings['service']
 
623
            https_cn = urlparse.urlparse(settings['internal_url'])
 
624
            https_cn = https_cn.hostname
 
625
    else:
 
626
        # assemble multiple endpoints from relation data. service name
 
627
        # should be prepended to setting name, ie:
 
628
        #  realtion-set ec2_service=$foo ec2_region=$foo ec2_public_url=$foo
 
629
        #  relation-set nova_service=$foo nova_region=$foo nova_public_url=$foo
 
630
        # Results in a dict that looks like:
 
631
        # { 'ec2': {
 
632
        #       'service': $foo
 
633
        #       'region': $foo
 
634
        #       'public_url': $foo
 
635
        #   }
 
636
        #   'nova': {
 
637
        #       'service': $foo
 
638
        #       'region': $foo
 
639
        #       'public_url': $foo
 
640
        #   }
 
641
        # }
 
642
        endpoints = {}
 
643
        for k, v in settings.iteritems():
 
644
            ep = k.split('_')[0]
 
645
            x = '_'.join(k.split('_')[1:])
 
646
            if ep not in endpoints:
 
647
                endpoints[ep] = {}
 
648
            endpoints[ep][x] = v
 
649
        services = []
 
650
        https_cn = None
 
651
        for ep in endpoints:
 
652
            # weed out any unrelated relation stuff Juju might have added
 
653
            # by ensuring each possible endpiont has appropriate fields
 
654
            #  ['service', 'region', 'public_url', 'admin_url', 'internal_url']
 
655
            if single.issubset(endpoints[ep]):
 
656
                ensure_valid_service(ep['service'])
 
657
                ep = endpoints[ep]
 
658
                add_endpoint(region=ep['region'], service=ep['service'],
 
659
                             publicurl=ep['public_url'],
 
660
                             adminurl=ep['admin_url'],
 
661
                             internalurl=ep['internal_url'])
 
662
                services.append(ep['service'])
 
663
                if not https_cn:
 
664
                    https_cn = urlparse.urlparse(ep['internal_url'])
 
665
                    https_cn = https_cn.hostname
 
666
        service_username = '_'.join(services)
 
667
 
 
668
    if 'None' in [v for k, v in settings.iteritems()]:
 
669
        return
 
670
 
 
671
    if not service_username:
 
672
        return
 
673
 
 
674
    token = get_admin_token()
 
675
    log("Creating service credentials for '%s'" % service_username)
 
676
 
 
677
    service_password = get_service_password(service_username)
 
678
    create_user(service_username, service_password, config('service-tenant'))
 
679
    grant_role(service_username, config('admin-role'),
 
680
               config('service-tenant'))
 
681
 
 
682
    # Allow the remote service to request creation of any additional roles.
 
683
    # Currently used by Swift and Ceilometer.
 
684
    for role in get_requested_roles(settings):
 
685
        log("Creating requested role: %s" % role)
 
686
        create_role(role, service_username,
 
687
                    config('service-tenant'))
 
688
 
 
689
    # As of https://review.openstack.org/#change,4675, all nodes hosting
 
690
    # an endpoint(s) needs a service username and password assigned to
 
691
    # the service tenant and granted admin role.
 
692
    # note: config('service-tenant') is created in utils.ensure_initial_admin()
 
693
    # we return a token, information about our API endpoints, and the generated
 
694
    # service credentials
 
695
    relation_data = {
 
696
        "admin_token": token,
 
697
        "service_host": config("hostname"),
 
698
        "service_port": config("service-port"),
 
699
        "auth_host": config("hostname"),
 
700
        "auth_port": config("admin-port"),
 
701
        "service_username": service_username,
 
702
        "service_password": service_password,
 
703
        "service_tenant": config('service-tenant'),
 
704
        "https_keystone": "False",
 
705
        "ssl_cert": "",
 
706
        "ssl_key": "",
 
707
        "ca_cert": ""
 
708
    }
 
709
 
 
710
    # Check if clustered and use vip + haproxy ports if so
 
711
    if is_clustered():
 
712
        relation_data["auth_host"] = config('vip')
 
713
        relation_data["service_host"] = config('vip')
 
714
 
 
715
    # generate or get a new cert/key for service if set to manage certs.
 
716
    if config('https-service-endpoints') in ['True', 'true']:
 
717
        ca = get_ca(user=SSH_USER)
 
718
        cert, key = ca.get_cert_and_key(common_name=https_cn)
 
719
        ca_bundle = ca.get_ca_bundle()
 
720
        relation_data['ssl_cert'] = b64encode(cert)
 
721
        relation_data['ssl_key'] = b64encode(key)
 
722
        relation_data['ca_cert'] = b64encode(ca_bundle)
 
723
        relation_data['https_keystone'] = 'True'
 
724
        if is_clustered():
 
725
            unison.sync_to_peers(peer_interface='cluster',
 
726
                                 paths=[SSL_DIR], user=SSH_USER, verbose=True)
 
727
    relation_set(**relation_data)
 
728
 
 
729
 
 
730
def ensure_valid_service(service):
 
731
    if service not in valid_services.keys():
 
732
        log("Invalid service requested: '%s'" % service)
 
733
        relation_set(admin_token=-1)
 
734
        return
 
735
 
 
736
 
 
737
def add_endpoint(region, service, publicurl, adminurl, internalurl):
 
738
    desc = valid_services[service]["desc"]
 
739
    service_type = valid_services[service]["type"]
 
740
    create_service_entry(service, service_type, desc)
 
741
    create_endpoint_template(region=region, service=service,
 
742
                             publicurl=publicurl,
 
743
                             adminurl=adminurl,
 
744
                             internalurl=internalurl)
 
745
 
 
746
 
 
747
def get_requested_roles(settings):
 
748
    ''' Retrieve any valid requested_roles from dict settings '''
 
749
    if ('requested_roles' in settings and
 
750
        settings['requested_roles'] not in ['None', None]):
 
751
        return settings['requested_roles'].split(',')
 
752
    else:
 
753
        return []