Deploy from source

This commit is contained in:
Corey Bryant
2015-04-07 13:58:41 +00:00
parent c94e33aa81
commit 792153ae93
50 changed files with 8850 additions and 113 deletions

View File

@@ -1,2 +1,3 @@
.coverage
bin
tags

View File

@@ -2,7 +2,7 @@
PYTHON := /usr/bin/env python
lint:
@flake8 --exclude hooks/charmhelpers hooks unit_tests tests
@flake8 --exclude hooks/charmhelpers actions hooks unit_tests tests
@charm proof
unit_test:
@@ -15,7 +15,7 @@ bin/charm_helpers_sync.py:
> bin/charm_helpers_sync.py
sync: bin/charm_helpers_sync.py
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers.yaml
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-hooks.yaml
@$(PYTHON) bin/charm_helpers_sync.py -c charm-helpers-tests.yaml
test:
@@ -25,7 +25,8 @@ test:
# https://bugs.launchpad.net/amulet/+bug/1320357
@juju test -v -p AMULET_HTTP_PROXY --timeout 900 \
00-setup 14-basic-precise-icehouse 15-basic-trusty-icehouse \
16-basic-trusty-juno
16-basic-trusty-icehouse-git 17-basic-trusty-juno \
18-basic-trusty-juno-git
publish: lint test
bzr push lp:charms/openstack-dashboard

151
README.md
View File

@@ -66,3 +66,154 @@ to also deploy the dashboard with load balancing proxy such as HAProxy:
This option potentially provides better scale-out than using the charm in
conjunction with the hacluster charm.
Deploying from source
=====================
The minimum openstack-origin-git config required to deploy from source is:
openstack-origin-git:
"repositories:
- {name: requirements,
repository: 'git://git.openstack.org/openstack/requirements',
branch: stable/juno}
- {name: horizon,
repository: 'git://git.openstack.org/openstack/horizon',
branch: stable/juno}"
Note that there are only two 'name' values the charm knows about: 'requirements'
and 'horizon'. These repositories must correspond to these 'name' values.
Additionally, the requirements repository must be specified first and the
horizon repository must be specified last. All other repostories are installed
in the order in which they are specified.
The following is a full list of current tip repos (may not be up-to-date):
openstack-origin-git:
"repositories:
- {name: requirements,
repository: 'git://git.openstack.org/openstack/requirements',
branch: master}
- {name: oslo-concurrency,
repository: 'git://git.openstack.org/openstack/oslo.concurrency',
branch: master}
- {name: oslo-config,
repository: 'git://git.openstack.org/openstack/oslo.config',
branch: master}
- {name: oslo-i18n,
repository: 'git://git.openstack.org/openstack/oslo.i18n',
branch: master}
- {name: oslo-serialization,
repository: 'git://git.openstack.org/openstack/oslo.serialization',
branch: master}
- {name: oslo-utils,
repository: 'git://git.openstack.org/openstack/oslo.utils',
branch: master}
- {name: pbr,
repository: 'git://git.openstack.org/openstack-dev/pbr',
branch: master}
- {name: python-ceilometerclient,
repository: 'git://git.openstack.org/openstack/python-ceilometerclient',
branch: master}
- {name: python-cinderclient,
repository: 'git://git.openstack.org/openstack/python-cinderclient',
branch: master}
- {name: python-glanceclient,
repository: 'git://git.openstack.org/openstack/python-glanceclient',
branch: master}
- {name: python-heatclient,
repository: 'git://git.openstack.org/openstack/python-heatclient',
branch: master}
- {name: python-keystoneclient,
repository: 'git://git.openstack.org/openstack/python-keystoneclient',
branch: master}
- {name: python-neutronclient,
repository: 'git://git.openstack.org/openstack/python-neutronclient',
branch: master}
- {name: python-novaclient,
repository: 'git://git.openstack.org/openstack/python-novaclient',
branch: master}
- {name: python-saharaclient,
repository: 'git://git.openstack.org/openstack/python-saharaclient',
branch: master}
- {name: python-swiftclient,
repository: 'git://git.openstack.org/openstack/python-swiftclient',
branch: master}
- {name: python-troveclient,
repository: 'git://git.openstack.org/openstack/python-troveclient',
branch: master}
- {name: xstatic-angular,
repository: 'git://git.openstack.org/stackforge/xstatic-angular',
branch: master}
- {name: xstatic-angular-animate,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-animate',
branch: master}
- {name: xstatic-angular-bootstrap,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-bootstrap',
branch: master}
- {name: xstatic-angular-cookies,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-cookies',
branch: master}
- {name: xstatic-angular-fileupload,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-fileupload',
branch: master}
- {name: xstatic-angular-lrdragndrop,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-lrdragndrop',
branch: master}
- {name: xstatic-angular-mock,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-mock',
branch: master}
- {name: xstatic-angular-sanitize,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-sanitize',
branch: master}
- {name: xstatic-angular-smart-table,
repository: 'git://git.openstack.org/stackforge/xstatic-angular-smart-table',
branch: master}
- {name: xstatic-bootstrap-datepicker,
repository: 'git://git.openstack.org/stackforge/xstatic-bootstrap-datepicker',
branch: master}
- {name: xstatic-bootstrap-scss,
repository: 'git://git.openstack.org/stackforge/xstatic-bootstrap-scss',
branch: master}
- {name: xstatic-d3,
repository: 'git://git.openstack.org/stackforge/xstatic-d3',
branch: master}
- {name: xstatic-font-awesome,
repository: 'git://git.openstack.org/stackforge/xstatic-font-awesome',
branch: master}
- {name: xstatic-hogan,
repository: 'git://git.openstack.org/stackforge/xstatic-hogan',
branch: master}
- {name: xstatic-jasmine,
repository: 'git://git.openstack.org/stackforge/xstatic-jasmine',
branch: master}
- {name: xstatic-jquery-migrate,
repository: 'git://git.openstack.org/stackforge/xstatic-jquery-migrate',
branch: master}
- {name: xstatic-jquery.bootstrap.wizard,
repository: 'git://git.openstack.org/stackforge/xstatic-jquery.bootstrap.wizard',
branch: master}
- {name: xstatic-jquery.quicksearch,
repository: 'git://git.openstack.org/stackforge/xstatic-jquery.quicksearch',
branch: master}
- {name: xstatic-jquery.tablesorter,
repository: 'git://git.openstack.org/stackforge/xstatic-jquery.tablesorter',
branch: master}
- {name: xstatic-jsencrypt,
repository: 'git://git.openstack.org/stackforge/xstatic-jsencrypt',
branch: master}
- {name: xstatic-magic-search,
repository: 'git://git.openstack.org/stackforge/xstatic-magic-search',
branch: master}
- {name: xstatic-qunit,
repository: 'git://git.openstack.org/stackforge/xstatic-qunit',
branch: master}
- {name: xstatic-rickshaw,
repository: 'git://git.openstack.org/stackforge/xstatic-rickshaw',
branch: master}
- {name: xstatic-spin,
repository: 'git://git.openstack.org/stackforge/xstatic-spin',
branch: master}
- {name: horizon,
repository: 'git://git.openstack.org/openstack/horizon',
branch: master}"

2
actions.yaml Normal file
View File

@@ -0,0 +1,2 @@
git-reinstall:
description: Reinstall openstack-dashboard from the openstack-origin-git repositories.

1
actions/git-reinstall Symbolic link
View File

@@ -0,0 +1 @@
git_reinstall.py

45
actions/git_reinstall.py Executable file
View File

@@ -0,0 +1,45 @@
#!/usr/bin/python
import sys
import traceback
sys.path.append('hooks/')
from charmhelpers.contrib.openstack.utils import (
git_install_requested,
)
from charmhelpers.core.hookenv import (
action_set,
action_fail,
config,
)
from horizon_utils import (
git_install,
)
from horizon_hooks import (
config_changed,
)
def git_reinstall():
"""Reinstall from source and restart services.
If the openstack-origin-git config option was used to install openstack
from source git repositories, then this action can be used to reinstall
from updated git repositories, followed by a restart of services."""
if not git_install_requested():
action_fail('openstack-origin-git is not configured')
return
try:
git_install(config('openstack-origin-git'))
except:
action_set({'traceback': traceback.format_exc()})
action_fail('git-reinstall resulted in an unexpected error')
if __name__ == '__main__':
git_reinstall()
config_changed()

View File

@@ -14,6 +14,22 @@ options:
Note that updating this setting to a source that is known to
provide a later version of OpenStack will trigger a software
upgrade.
Note that when openstack-origin-git is specified, openstack
specific packages will be installed from source rather than
from the openstack-origin repository.
openstack-origin-git:
default:
type: string
description: |
Specifies a YAML-formatted dictionary listing the git
repositories and branches from which to install OpenStack and
its dependencies.
Note that the installed config files will be determined based on
the OpenStack release of the openstack-origin option.
For more details see README.md.
webroot:
default: "/horizon"
type: string

View File

@@ -15,6 +15,7 @@
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import six
from collections import OrderedDict
from charmhelpers.contrib.amulet.deployment import (
AmuletDeployment
)
@@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment):
"""
(self.precise_essex, self.precise_folsom, self.precise_grizzly,
self.precise_havana, self.precise_icehouse,
self.trusty_icehouse) = range(6)
self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8)
releases = {
('precise', None): self.precise_essex,
('precise', 'cloud:precise-folsom'): self.precise_folsom,
('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
('precise', 'cloud:precise-havana'): self.precise_havana,
('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
('trusty', None): self.trusty_icehouse}
('trusty', None): self.trusty_icehouse,
('trusty', 'cloud:trusty-juno'): self.trusty_juno,
('trusty', 'cloud:trusty-kilo'): self.trusty_kilo}
return releases[(self.series, self.openstack)]
def _get_openstack_release_string(self):
"""Get openstack release string.
Return a string representing the openstack release.
"""
releases = OrderedDict([
('precise', 'essex'),
('quantal', 'folsom'),
('raring', 'grizzly'),
('saucy', 'havana'),
('trusty', 'icehouse'),
('utopic', 'juno'),
('vivid', 'kilo'),
])
if self.openstack:
os_origin = self.openstack.split(':')[1]
return os_origin.split('%s-' % self.series)[1].split('/')[0]
else:
return releases[self.series]

View File

@@ -16,6 +16,7 @@
import json
import os
import re
import time
from base64 import b64decode
from subprocess import check_call
@@ -46,8 +47,11 @@ from charmhelpers.core.hookenv import (
)
from charmhelpers.core.sysctl import create as sysctl_create
from charmhelpers.core.strutils import bool_from_string
from charmhelpers.core.host import (
list_nics,
get_nic_hwaddr,
mkdir,
write_file,
)
@@ -64,16 +68,22 @@ from charmhelpers.contrib.hahelpers.apache import (
)
from charmhelpers.contrib.openstack.neutron import (
neutron_plugin_attribute,
parse_data_port_mappings,
)
from charmhelpers.contrib.openstack.ip import (
resolve_address,
INTERNAL,
)
from charmhelpers.contrib.network.ip import (
get_address_in_network,
get_ipv4_addr,
get_ipv6_addr,
get_netmask_for_address,
format_ipv6_addr,
is_address_in_network,
is_bridge_member,
)
from charmhelpers.contrib.openstack.utils import get_host_ip
CA_CERT_PATH = '/usr/local/share/ca-certificates/keystone_juju_ca_cert.crt'
ADDRESS_TYPES = ['admin', 'internal', 'public']
@@ -310,14 +320,15 @@ def db_ssl(rdata, ctxt, ssl_dir):
class IdentityServiceContext(OSContextGenerator):
interfaces = ['identity-service']
def __init__(self, service=None, service_user=None):
def __init__(self, service=None, service_user=None, rel_name='identity-service'):
self.service = service
self.service_user = service_user
self.rel_name = rel_name
self.interfaces = [self.rel_name]
def __call__(self):
log('Generating template context for identity-service', level=DEBUG)
log('Generating template context for ' + self.rel_name, level=DEBUG)
ctxt = {}
if self.service and self.service_user:
@@ -331,7 +342,7 @@ class IdentityServiceContext(OSContextGenerator):
ctxt['signing_dir'] = cachedir
for rid in relation_ids('identity-service'):
for rid in relation_ids(self.rel_name):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
serv_host = rdata.get('service_host')
@@ -727,7 +738,14 @@ class ApacheSSLContext(OSContextGenerator):
'endpoints': [],
'ext_ports': []}
for cn in self.canonical_names():
cns = self.canonical_names()
if cns:
for cn in cns:
self.configure_cert(cn)
else:
# Expect cert/key provided in config (currently assumed that ca
# uses ip for cn)
cn = resolve_address(endpoint_type=INTERNAL)
self.configure_cert(cn)
addresses = self.get_network_addresses()
@@ -883,6 +901,48 @@ class NeutronContext(OSContextGenerator):
return ctxt
class NeutronPortContext(OSContextGenerator):
NIC_PREFIXES = ['eth', 'bond']
def resolve_ports(self, ports):
"""Resolve NICs not yet bound to bridge(s)
If hwaddress provided then returns resolved hwaddress otherwise NIC.
"""
if not ports:
return None
hwaddr_to_nic = {}
hwaddr_to_ip = {}
for nic in list_nics(self.NIC_PREFIXES):
hwaddr = get_nic_hwaddr(nic)
hwaddr_to_nic[hwaddr] = nic
addresses = get_ipv4_addr(nic, fatal=False)
addresses += get_ipv6_addr(iface=nic, fatal=False)
hwaddr_to_ip[hwaddr] = addresses
resolved = []
mac_regex = re.compile(r'([0-9A-F]{2}[:-]){5}([0-9A-F]{2})', re.I)
for entry in ports:
if re.match(mac_regex, entry):
# NIC is in known NICs and does NOT hace an IP address
if entry in hwaddr_to_nic and not hwaddr_to_ip[entry]:
# If the nic is part of a bridge then don't use it
if is_bridge_member(hwaddr_to_nic[entry]):
continue
# Entry is a MAC address for a valid interface that doesn't
# have an IP address assigned yet.
resolved.append(hwaddr_to_nic[entry])
else:
# If the passed entry is not a MAC address, assume it's a valid
# interface, and that the user put it there on purpose (we can
# trust it to be the real external network).
resolved.append(entry)
return resolved
class OSConfigFlagContext(OSContextGenerator):
"""Provides support for user-defined config flags.
@@ -1104,3 +1164,145 @@ class SysctlContext(OSContextGenerator):
sysctl_create(sysctl_dict,
'/etc/sysctl.d/50-{0}.conf'.format(charm_name()))
return {'sysctl': sysctl_dict}
class NeutronAPIContext(OSContextGenerator):
'''
Inspects current neutron-plugin-api relation for neutron settings. Return
defaults if it is not present.
'''
interfaces = ['neutron-plugin-api']
def __call__(self):
self.neutron_defaults = {
'l2_population': {
'rel_key': 'l2-population',
'default': False,
},
'overlay_network_type': {
'rel_key': 'overlay-network-type',
'default': 'gre',
},
'neutron_security_groups': {
'rel_key': 'neutron-security-groups',
'default': False,
},
'network_device_mtu': {
'rel_key': 'network-device-mtu',
'default': None,
},
'enable_dvr': {
'rel_key': 'enable-dvr',
'default': False,
},
'enable_l3ha': {
'rel_key': 'enable-l3ha',
'default': False,
},
}
ctxt = self.get_neutron_options({})
for rid in relation_ids('neutron-plugin-api'):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
if 'l2-population' in rdata:
ctxt.update(self.get_neutron_options(rdata))
return ctxt
def get_neutron_options(self, rdata):
settings = {}
for nkey in self.neutron_defaults.keys():
defv = self.neutron_defaults[nkey]['default']
rkey = self.neutron_defaults[nkey]['rel_key']
if rkey in rdata.keys():
if type(defv) is bool:
settings[nkey] = bool_from_string(rdata[rkey])
else:
settings[nkey] = rdata[rkey]
else:
settings[nkey] = defv
return settings
class ExternalPortContext(NeutronPortContext):
def __call__(self):
ctxt = {}
ports = config('ext-port')
if ports:
ports = [p.strip() for p in ports.split()]
ports = self.resolve_ports(ports)
if ports:
ctxt = {"ext_port": ports[0]}
napi_settings = NeutronAPIContext()()
mtu = napi_settings.get('network_device_mtu')
if mtu:
ctxt['ext_port_mtu'] = mtu
return ctxt
class DataPortContext(NeutronPortContext):
def __call__(self):
ports = config('data-port')
if ports:
portmap = parse_data_port_mappings(ports)
ports = portmap.values()
resolved = self.resolve_ports(ports)
normalized = {get_nic_hwaddr(port): port for port in resolved
if port not in ports}
normalized.update({port: port for port in resolved
if port in ports})
if resolved:
return {bridge: normalized[port] for bridge, port in
six.iteritems(portmap) if port in normalized.keys()}
return None
class PhyNICMTUContext(DataPortContext):
def __call__(self):
ctxt = {}
mappings = super(PhyNICMTUContext, self).__call__()
if mappings and mappings.values():
ports = mappings.values()
napi_settings = NeutronAPIContext()()
mtu = napi_settings.get('network_device_mtu')
if mtu:
ctxt["devs"] = '\\n'.join(ports)
ctxt['mtu'] = mtu
return ctxt
class NetworkServiceContext(OSContextGenerator):
def __init__(self, rel_name='quantum-network-service'):
self.rel_name = rel_name
self.interfaces = [rel_name]
def __call__(self):
for rid in relation_ids(self.rel_name):
for unit in related_units(rid):
rdata = relation_get(rid=rid, unit=unit)
ctxt = {
'keystone_host': rdata.get('keystone_host'),
'service_port': rdata.get('service_port'),
'auth_port': rdata.get('auth_port'),
'service_tenant': rdata.get('service_tenant'),
'service_username': rdata.get('service_username'),
'service_password': rdata.get('service_password'),
'quantum_host': rdata.get('quantum_host'),
'quantum_port': rdata.get('quantum_port'),
'quantum_url': rdata.get('quantum_url'),
'region': rdata.get('region'),
'service_protocol':
rdata.get('service_protocol') or 'http',
'auth_protocol':
rdata.get('auth_protocol') or 'http',
}
if context_complete(ctxt):
return ctxt
return {}

View File

@@ -16,6 +16,7 @@
# Various utilies for dealing with Neutron and the renaming from Quantum.
import six
from subprocess import check_output
from charmhelpers.core.hookenv import (
@@ -237,3 +238,72 @@ def network_manager():
else:
# ensure accurate naming for all releases post-H
return 'neutron'
def parse_mappings(mappings):
parsed = {}
if mappings:
mappings = mappings.split(' ')
for m in mappings:
p = m.partition(':')
if p[1] == ':':
parsed[p[0].strip()] = p[2].strip()
return parsed
def parse_bridge_mappings(mappings):
"""Parse bridge mappings.
Mappings must be a space-delimited list of provider:bridge mappings.
Returns dict of the form {provider:bridge}.
"""
return parse_mappings(mappings)
def parse_data_port_mappings(mappings, default_bridge='br-data'):
"""Parse data port mappings.
Mappings must be a space-delimited list of bridge:port mappings.
Returns dict of the form {bridge:port}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
if not mappings:
return {}
# For backwards-compatibility we need to support port-only provided in
# config.
_mappings = {default_bridge: mappings.split(' ')[0]}
bridges = _mappings.keys()
ports = _mappings.values()
if len(set(bridges)) != len(bridges):
raise Exception("It is not allowed to have more than one port "
"configured on the same bridge")
if len(set(ports)) != len(ports):
raise Exception("It is not allowed to have the same port configured "
"on more than one bridge")
return _mappings
def parse_vlan_range_mappings(mappings):
"""Parse vlan range mappings.
Mappings must be a space-delimited list of provider:start:end mappings.
Returns dict of the form {provider: (start, end)}.
"""
_mappings = parse_mappings(mappings)
if not _mappings:
return {}
mappings = {}
for p, r in six.iteritems(_mappings):
mappings[p] = tuple(r.split(':'))
return mappings

View File

@@ -0,0 +1,13 @@
description "{{ service_description }}"
author "Juju {{ service_name }} Charm <juju@localhost>"
start on runlevel [2345]
stop on runlevel [!2345]
respawn
exec start-stop-daemon --start --chuid {{ user_name }} \
--chdir {{ start_dir }} --name {{ process_name }} \
--exec {{ executable_name }} -- \
--config-file={{ config_file }} \
--log-file={{ log_file }}

View File

@@ -0,0 +1,9 @@
{% if auth_host -%}
[keystone_authtoken]
identity_uri = {{ auth_protocol }}://{{ auth_host }}:{{ auth_port }}/{{ auth_admin_prefix }}
auth_uri = {{ service_protocol }}://{{ service_host }}:{{ service_port }}/{{ service_admin_prefix }}
admin_tenant_name = {{ admin_tenant_name }}
admin_user = {{ admin_user }}
admin_password = {{ admin_password }}
signing_dir = {{ signing_dir }}
{% endif -%}

View File

@@ -0,0 +1,22 @@
{% if rabbitmq_host or rabbitmq_hosts -%}
[oslo_messaging_rabbit]
rabbit_userid = {{ rabbitmq_user }}
rabbit_virtual_host = {{ rabbitmq_virtual_host }}
rabbit_password = {{ rabbitmq_password }}
{% if rabbitmq_hosts -%}
rabbit_hosts = {{ rabbitmq_hosts }}
{% if rabbitmq_ha_queues -%}
rabbit_ha_queues = True
rabbit_durable_queues = False
{% endif -%}
{% else -%}
rabbit_host = {{ rabbitmq_host }}
{% endif -%}
{% if rabbit_ssl_port -%}
rabbit_use_ssl = True
rabbit_port = {{ rabbit_ssl_port }}
{% if rabbit_ssl_ca -%}
kombu_ssl_ca_certs = {{ rabbit_ssl_ca }}
{% endif -%}
{% endif -%}
{% endif -%}

View File

@@ -3,12 +3,12 @@
rpc_backend = zmq
rpc_zmq_host = {{ zmq_host }}
{% if zmq_redis_address -%}
rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_redis.MatchMakerRedis
rpc_zmq_matchmaker = redis
matchmaker_heartbeat_freq = 15
matchmaker_heartbeat_ttl = 30
[matchmaker_redis]
host = {{ zmq_redis_address }}
{% else -%}
rpc_zmq_matchmaker = oslo.messaging._drivers.matchmaker_ring.MatchMakerRing
rpc_zmq_matchmaker = ring
{% endif -%}
{% endif -%}

View File

@@ -30,6 +30,10 @@ import yaml
from charmhelpers.contrib.network import ip
from charmhelpers.core import (
unitdata,
)
from charmhelpers.core.hookenv import (
config,
log as juju_log,
@@ -330,6 +334,21 @@ def configure_installation_source(rel):
error_out("Invalid openstack-release specified: %s" % rel)
def config_value_changed(option):
"""
Determine if config value changed since last call to this function.
"""
hook_data = unitdata.HookData()
with hook_data():
db = unitdata.kv()
current = config(option)
saved = db.get(option)
db.set(option, current)
if saved is None:
return False
return current != saved
def save_script_rc(script_path="scripts/scriptrc", **env_vars):
"""
Write an rc file in the charm-delivered directory containing
@@ -469,82 +488,103 @@ def os_requires_version(ostack_release, pkg):
def git_install_requested():
"""Returns true if openstack-origin-git is specified."""
return config('openstack-origin-git') != "None"
"""
Returns true if openstack-origin-git is specified.
"""
return config('openstack-origin-git') is not None
requirements_dir = None
def git_clone_and_install(file_name, core_project):
"""Clone/install all OpenStack repos specified in yaml config file."""
global requirements_dir
def git_clone_and_install(projects_yaml, core_project):
"""
Clone/install all specified OpenStack repositories.
if file_name == "None":
The expected format of projects_yaml is:
repositories:
- {name: keystone,
repository: 'git://git.openstack.org/openstack/keystone.git',
branch: 'stable/icehouse'}
- {name: requirements,
repository: 'git://git.openstack.org/openstack/requirements.git',
branch: 'stable/icehouse'}
directory: /mnt/openstack-git
http_proxy: http://squid.internal:3128
https_proxy: https://squid.internal:3128
The directory, http_proxy, and https_proxy keys are optional.
"""
global requirements_dir
parent_dir = '/mnt/openstack-git'
if not projects_yaml:
return
yaml_file = os.path.join(charm_dir(), file_name)
projects = yaml.load(projects_yaml)
_git_validate_projects_yaml(projects, core_project)
# clone/install the requirements project first
installed = _git_clone_and_install_subset(yaml_file,
whitelist=['requirements'])
if 'requirements' not in installed:
error_out('requirements git repository must be specified')
if 'http_proxy' in projects.keys():
os.environ['http_proxy'] = projects['http_proxy']
# clone/install all other projects except requirements and the core project
blacklist = ['requirements', core_project]
_git_clone_and_install_subset(yaml_file, blacklist=blacklist,
update_requirements=True)
if 'https_proxy' in projects.keys():
os.environ['https_proxy'] = projects['https_proxy']
# clone/install the core project
whitelist = [core_project]
installed = _git_clone_and_install_subset(yaml_file, whitelist=whitelist,
update_requirements=True)
if core_project not in installed:
error_out('{} git repository must be specified'.format(core_project))
if 'directory' in projects.keys():
parent_dir = projects['directory']
for p in projects['repositories']:
repo = p['repository']
branch = p['branch']
if p['name'] == 'requirements':
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
update_requirements=False)
requirements_dir = repo_dir
else:
repo_dir = _git_clone_and_install_single(repo, branch, parent_dir,
update_requirements=True)
def _git_clone_and_install_subset(yaml_file, whitelist=[], blacklist=[],
update_requirements=False):
"""Clone/install subset of OpenStack repos specified in yaml config file."""
global requirements_dir
installed = []
def _git_validate_projects_yaml(projects, core_project):
"""
Validate the projects yaml.
"""
_git_ensure_key_exists('repositories', projects)
with open(yaml_file, 'r') as fd:
projects = yaml.load(fd)
for proj, val in projects.items():
# The project subset is chosen based on the following 3 rules:
# 1) If project is in blacklist, we don't clone/install it, period.
# 2) If whitelist is empty, we clone/install everything else.
# 3) If whitelist is not empty, we clone/install everything in the
# whitelist.
if proj in blacklist:
continue
if whitelist and proj not in whitelist:
continue
repo = val['repository']
branch = val['branch']
repo_dir = _git_clone_and_install_single(repo, branch,
update_requirements)
if proj == 'requirements':
requirements_dir = repo_dir
installed.append(proj)
return installed
for project in projects['repositories']:
_git_ensure_key_exists('name', project.keys())
_git_ensure_key_exists('repository', project.keys())
_git_ensure_key_exists('branch', project.keys())
if projects['repositories'][0]['name'] != 'requirements':
error_out('{} git repo must be specified first'.format('requirements'))
if projects['repositories'][-1]['name'] != core_project:
error_out('{} git repo must be specified last'.format(core_project))
def _git_clone_and_install_single(repo, branch, update_requirements=False):
"""Clone and install a single git repository."""
dest_parent_dir = "/mnt/openstack-git/"
dest_dir = os.path.join(dest_parent_dir, os.path.basename(repo))
def _git_ensure_key_exists(key, keys):
"""
Ensure that key exists in keys.
"""
if key not in keys:
error_out('openstack-origin-git key \'{}\' is missing'.format(key))
if not os.path.exists(dest_parent_dir):
juju_log('Host dir not mounted at {}. '
'Creating directory there instead.'.format(dest_parent_dir))
os.mkdir(dest_parent_dir)
def _git_clone_and_install_single(repo, branch, parent_dir, update_requirements):
"""
Clone and install a single git repository.
"""
dest_dir = os.path.join(parent_dir, os.path.basename(repo))
if not os.path.exists(parent_dir):
juju_log('Directory already exists at {}. '
'No need to create directory.'.format(parent_dir))
os.mkdir(parent_dir)
if not os.path.exists(dest_dir):
juju_log('Cloning git repo: {}, branch: {}'.format(repo, branch))
repo_dir = install_remote(repo, dest=dest_parent_dir, branch=branch)
repo_dir = install_remote(repo, dest=parent_dir, branch=branch)
else:
repo_dir = dest_dir
@@ -561,16 +601,39 @@ def _git_clone_and_install_single(repo, branch, update_requirements=False):
def _git_update_requirements(package_dir, reqs_dir):
"""Update from global requirements.
"""
Update from global requirements.
Update an OpenStack git directory's requirements.txt and
test-requirements.txt from global-requirements.txt."""
Update an OpenStack git directory's requirements.txt and
test-requirements.txt from global-requirements.txt.
"""
orig_dir = os.getcwd()
os.chdir(reqs_dir)
cmd = "python update.py {}".format(package_dir)
cmd = ['python', 'update.py', package_dir]
try:
subprocess.check_call(cmd.split(' '))
subprocess.check_call(cmd)
except subprocess.CalledProcessError:
package = os.path.basename(package_dir)
error_out("Error updating {} from global-requirements.txt".format(package))
os.chdir(orig_dir)
def git_src_dir(projects_yaml, project):
"""
Return the directory where the specified project's source is located.
"""
parent_dir = '/mnt/openstack-git'
if not projects_yaml:
return
projects = yaml.load(projects_yaml)
if 'directory' in projects.keys():
parent_dir = projects['directory']
for p in projects['repositories']:
if p['name'] == project:
return os.path.join(parent_dir, os.path.basename(p['repository']))
return None

View File

@@ -566,3 +566,29 @@ class Hooks(object):
def charm_dir():
"""Return the root directory of the current charm"""
return os.environ.get('CHARM_DIR')
@cached
def action_get(key=None):
"""Gets the value of an action parameter, or all key/value param pairs"""
cmd = ['action-get']
if key is not None:
cmd.append(key)
cmd.append('--format=json')
action_data = json.loads(subprocess.check_output(cmd).decode('UTF-8'))
return action_data
def action_set(values):
"""Sets the values to be returned after the action finishes"""
cmd = ['action-set']
for k, v in list(values.items()):
cmd.append('{}={}'.format(k, v))
subprocess.check_call(cmd)
def action_fail(message):
"""Sets the action status to failed and sets the error message.
The results set by action_set are preserved."""
subprocess.check_call(['action-fail', message])

View File

@@ -339,12 +339,16 @@ def lsb_release():
def pwgen(length=None):
"""Generate a random pasword."""
if length is None:
# A random length is ok to use a weak PRNG
length = random.choice(range(35, 45))
alphanumeric_chars = [
l for l in (string.ascii_letters + string.digits)
if l not in 'l0QD1vAEIOUaeiou']
# Use a crypto-friendly PRNG (e.g. /dev/urandom) for making the
# actual password
random_generator = random.SystemRandom()
random_chars = [
random.choice(alphanumeric_chars) for _ in range(length)]
random_generator.choice(alphanumeric_chars) for _ in range(length)]
return(''.join(random_chars))

View File

@@ -139,7 +139,7 @@ class MysqlRelation(RelationContext):
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'user', 'password', 'database']
super(HttpRelation).__init__(self, *args, **kwargs)
RelationContext.__init__(self, *args, **kwargs)
class HttpRelation(RelationContext):
@@ -154,7 +154,7 @@ class HttpRelation(RelationContext):
def __init__(self, *args, **kwargs):
self.required_keys = ['host', 'port']
super(HttpRelation).__init__(self, *args, **kwargs)
RelationContext.__init__(self, *args, **kwargs)
def provide_data(self):
return {

View File

@@ -443,7 +443,7 @@ class HookData(object):
data = hookenv.execution_environment()
self.conf = conf_delta = self.kv.delta(data['conf'], 'config')
self.rels = rels_delta = self.kv.delta(data['rels'], 'rels')
self.kv.set('env', data['env'])
self.kv.set('env', dict(data['env']))
self.kv.set('unit', data['unit'])
self.kv.set('relid', data.get('relid'))
return conf_delta, rels_delta

View File

@@ -21,18 +21,23 @@ from charmhelpers.core.host import (
restart_on_change
)
from charmhelpers.contrib.openstack.utils import (
config_value_changed,
configure_installation_source,
git_install_requested,
openstack_upgrade_available,
os_release,
save_script_rc
)
from horizon_utils import (
PACKAGES, register_configs,
determine_packages,
register_configs,
restart_map,
services,
LOCAL_SETTINGS, HAPROXY_CONF,
enable_ssl,
do_openstack_upgrade,
git_install,
git_post_install_late,
setup_ipv6
)
from charmhelpers.contrib.network.ip import (
@@ -55,8 +60,9 @@ CONFIGS = register_configs()
def install():
execd_preinstall()
configure_installation_source(config('openstack-origin'))
apt_update(fatal=True)
packages = PACKAGES[:]
packages = determine_packages()
if os_release('openstack-dashboard') < 'icehouse':
packages += ['nodejs', 'node-less']
if lsb_release()['DISTRIB_CODENAME'] == 'precise':
@@ -64,12 +70,14 @@ def install():
apt_install('python-six', fatal=True)
apt_install(filter_installed_packages(packages), fatal=True)
git_install(config('openstack-origin-git'))
@hooks.hook('upgrade-charm')
@restart_on_change(restart_map())
def upgrade_charm():
execd_preinstall()
apt_install(filter_installed_packages(PACKAGES), fatal=True)
apt_install(filter_installed_packages(determine_packages()), fatal=True)
update_nrpe_config()
CONFIGS.write_all()
@@ -92,8 +100,13 @@ def config_changed():
for relid in relation_ids('identity-service'):
keystone_joined(relid)
enable_ssl()
if openstack_upgrade_available('openstack-dashboard'):
do_openstack_upgrade(configs=CONFIGS)
if git_install_requested():
if config_value_changed('openstack-origin-git'):
git_install(config('openstack-origin-git'))
else:
if openstack_upgrade_available('openstack-dashboard'):
do_openstack_upgrade(configs=CONFIGS)
env_vars = {
'OPENSTACK_URL_HORIZON':
@@ -111,6 +124,9 @@ def config_changed():
open_port(80)
open_port(443)
if git_install_requested():
git_post_install_late()
@hooks.hook('identity-service-relation-joined')
def keystone_joined(rel_id=None):

View File

@@ -1,24 +1,38 @@
# vim: set ts=4:et
import grp
import horizon_contexts
import charmhelpers.contrib.openstack.templating as templating
import charmhelpers.contrib.openstack.context as context
import subprocess
import os
import pwd
import subprocess
import shutil
from collections import OrderedDict
import charmhelpers.contrib.openstack.context as context
import charmhelpers.contrib.openstack.templating as templating
from charmhelpers.contrib.openstack.utils import (
get_os_codename_package,
configure_installation_source,
get_os_codename_install_source,
configure_installation_source
git_install_requested,
git_clone_and_install,
os_release,
git_src_dir,
)
from charmhelpers.core.hookenv import (
charm_dir,
config,
log
)
from charmhelpers.core.host import (
adduser,
add_group,
add_user_to_group,
cmp_pkgrevno,
lsb_release
lsb_release,
mkdir,
service_restart,
)
from charmhelpers.fetch import (
apt_upgrade,
apt_update,
@@ -26,10 +40,33 @@ from charmhelpers.fetch import (
apt_install
)
PACKAGES = [
"openstack-dashboard", "python-keystoneclient", "python-memcache",
"memcached", "haproxy", "python-novaclient",
"openstack-dashboard-ubuntu-theme"
BASE_PACKAGES = [
'haproxy',
'memcached',
'openstack-dashboard',
'openstack-dashboard-ubuntu-theme',
'python-keystoneclient',
'python-memcache',
'python-novaclient',
]
BASE_GIT_PACKAGES = [
'apache2',
'libapache2-mod-wsgi',
'libxml2-dev',
'libxslt1-dev',
'python-dev',
'python-pip',
'python-setuptools',
'zlib1g-dev',
]
# ubuntu packages that should not be installed when deploying from git
GIT_PACKAGE_BLACKLIST = [
'openstack-dashboard',
'openstack-dashboard-ubuntu-theme',
'python-keystoneclient',
'python-novaclient',
]
APACHE_CONF_DIR = "/etc/apache2"
@@ -100,8 +137,7 @@ CONFIG_FILES = OrderedDict([
def register_configs():
''' Register config files with their respective contexts. '''
release = get_os_codename_package('openstack-dashboard', fatal=False) or \
'essex'
release = os_release('openstack-dashboard')
configs = templating.OSConfigRenderer(templates_dir=TEMPLATES,
openstack_release=release)
@@ -169,6 +205,20 @@ def enable_ssl():
subprocess.call(['a2enmod', 'ssl'])
def determine_packages():
"""Determine packages to install"""
packages = BASE_PACKAGES
if git_install_requested():
packages.extend(BASE_GIT_PACKAGES)
# don't include packages that will be installed from git
packages = list(set(packages))
for p in GIT_PACKAGE_BLACKLIST:
packages.remove(p)
return list(set(packages))
def do_openstack_upgrade(configs):
"""
Perform an upgrade. Takes care of upgrading packages, rewriting
@@ -208,3 +258,153 @@ def setup_ipv6():
' main')
apt_update()
apt_install('haproxy/trusty-backports', fatal=True)
def git_install(projects_yaml):
"""Perform setup, and install git repos specified in yaml parameter."""
if git_install_requested():
git_pre_install()
git_clone_and_install(projects_yaml, core_project='horizon')
git_post_install(projects_yaml)
def git_pre_install():
"""Perform horizon pre-install setup."""
dirs = [
'/etc/openstack-dashboard',
'/usr/share/openstack-dashboard',
'/usr/share/openstack-dashboard/bin/less',
'/usr/share/openstack-dashboard-ubuntu-theme/static/ubuntu/css',
'/usr/share/openstack-dashboard-ubuntu-theme/static/ubuntu/img',
'/usr/share/openstack-dashboard-ubuntu-theme/templates',
'/var/lib/openstack-dashboard',
]
adduser('horizon', shell='/bin/bash', system_user=True)
subprocess.check_call(['usermod', '--home',
'/usr/share/openstack-dashboard/', 'horizon'])
add_group('horizon', system_group=True)
add_user_to_group('horizon', 'horizon')
for d in dirs:
if d is '/var/lib/openstack-dashboard':
mkdir(d, owner='horizon', group='horizon', perms=0700, force=False)
else:
mkdir(d, owner='root', group='root', perms=0755, force=False)
def git_post_install(projects_yaml):
"""Perform horizon post-install setup."""
src_dir = git_src_dir(projects_yaml, 'horizon')
release = os_release('openstack-dashboard')
templates_dir = os.path.join(charm_dir(), 'templates')
templates_rel_dir = os.path.join(templates_dir, release)
theme_dir = '/usr/share/openstack-dashboard-ubuntu-theme'
copy_files = {
'manage': {
'src': os.path.join(src_dir, 'manage.py'),
'dest': '/usr/share/openstack-dashboard/manage.py',
},
'settings': {
'src': os.path.join(src_dir, 'openstack_dashboard/settings.py'),
'dest': '/usr/share/openstack-dashboard/settings.py',
},
'local_settings_example': {
'src': os.path.join(src_dir, 'openstack_dashboard/local',
'local_settings.py.example'),
'dest': '/etc/openstack-dashboard/local_settings.py',
},
'openstack-dashboard': {
'src': os.path.join(templates_dir, 'dashboard.conf'),
'dest': '/etc/apache2/conf-available/openstack-dashboard.conf',
},
'ubuntu_theme': {
'src': os.path.join(templates_rel_dir, 'theme/ubuntu_theme.py'),
'dest': '/etc/openstack-dashboard/ubuntu_theme.py',
},
}
for name, files in copy_files.iteritems():
if os.path.exists(files['dest']):
os.remove(files['dest'])
shutil.copyfile(files['src'], files['dest'])
copy_trees = {
'openstack_dashboard': {
'src': os.path.join(src_dir, 'openstack_dashboard'),
'dest': '/usr/share/openstack-dashboard/openstack_dashboard',
},
'ubuntu_css': {
'src': os.path.join(templates_rel_dir, 'theme/css'),
'dest': os.path.join(theme_dir, 'static/ubuntu/css'),
},
'ubuntu_img': {
'src': os.path.join(templates_rel_dir, 'theme/img'),
'dest': os.path.join(theme_dir, 'static/ubuntu/img'),
},
'templates': {
'src': os.path.join(templates_rel_dir, 'theme/templates'),
'dest': os.path.join(theme_dir, 'templates'),
},
}
for name, dirs in copy_trees.iteritems():
if os.path.exists(dirs['dest']):
shutil.rmtree(dirs['dest'])
shutil.copytree(dirs['src'], dirs['dest'])
share_dir = '/usr/share/openstack-dashboard/openstack_dashboard'
symlinks = [
{'src': '/usr/share/openstack-dashboard/openstack_dashboard/static',
'link': '/usr/share/openstack-dashboard/static'},
{'src': '/etc/openstack-dashboard/ubuntu_theme.py',
'link': os.path.join(share_dir, 'local/ubuntu_theme.py')},
{'src': '/usr/bin/lessc',
'link': '/usr/share/openstack-dashboard/bin/less/lessc'},
{'src': '/usr/share/openstack-dashboard-ubuntu-theme/static/ubuntu',
'link': os.path.join(share_dir, 'static/ubuntu')},
{'src': '/etc/openstack-dashboard/local_settings.py',
'link': os.path.join(share_dir, 'local/local_settings.py')},
{'src':
'/usr/local/lib/python2.7/dist-packages/horizon/static/horizon/',
'link': os.path.join(share_dir, 'static/horizon')},
]
for s in symlinks:
if os.path.lexists(s['link']):
os.remove(s['link'])
os.symlink(s['src'], s['link'])
os.chmod('/var/lib/openstack-dashboard', 0o750)
os.chmod('/usr/share/openstack-dashboard/manage.py', 0o755),
subprocess.check_call(['/usr/share/openstack-dashboard/manage.py',
'collectstatic', '--noinput'])
subprocess.check_call(['/usr/share/openstack-dashboard/manage.py',
'compress', '--force'])
uid = pwd.getpwnam('horizon').pw_uid
gid = grp.getgrnam('horizon').gr_gid
os.chown('/etc/openstack-dashboard', uid, gid)
os.chown('/usr/share/openstack-dashboard/openstack_dashboard/static',
uid, gid)
os.chown('/var/lib/openstack-dashboard', uid, gid)
static_dir = '/usr/share/openstack-dashboard/openstack_dashboard/static'
for root, dirs, files in os.walk(static_dir):
for d in dirs:
os.lchown(os.path.join(root, d), uid, gid)
for f in files:
os.lchown(os.path.join(root, f), uid, gid)
subprocess.check_call(['a2enconf', 'openstack-dashboard'])
service_restart('apache2')
def git_post_install_late():
"""Perform horizon post-install setup."""
subprocess.check_call(['/usr/share/openstack-dashboard/manage.py',
'collectstatic', '--noinput'])
subprocess.check_call(['/usr/share/openstack-dashboard/manage.py',
'compress', '--force'])

8
templates/dashboard.conf Normal file
View File

@@ -0,0 +1,8 @@
WSGIScriptAlias /horizon /usr/share/openstack-dashboard/openstack_dashboard/wsgi/django.wsgi
WSGIDaemonProcess horizon user=horizon group=horizon processes=3 threads=10
WSGIProcessGroup horizon
Alias /static /usr/share/openstack-dashboard/openstack_dashboard/static/
<Directory /usr/share/openstack-dashboard/openstack_dashboard/wsgi>
Order allow,deny
Allow from all
</Directory>

File diff suppressed because it is too large Load Diff

Binary file not shown.

After

Width:  |  Height:  |  Size: 163 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 474 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,12 @@
{% load compress %}
{% compress css %}
<link href='{{ STATIC_URL }}dashboard/less/horizon.less' type='text/less' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/less/rickshaw.css' type='text/css' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/less/horizon_charts.less' type='text/less' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/less/horizon_workflow.less' type='text/less' media='screen' rel='stylesheet' />
{% endcompress %}
<link href='{{ STATIC_URL }}ubuntu/css/ubuntu.css' type='text/css' media='screen' rel='stylesheet' />
<link rel="shortcut icon" href="{{ STATIC_URL }}ubuntu/img/favicon-ubuntu.ico"/>

View File

@@ -0,0 +1,5 @@
# The presence of this file in /etc/openstack-dashboard/ and/or
# /usr/share/openstack-dashboard/openstack_dashboard/local/ will
# enable the Ubuntu theme for Horizon. To disable, remove the
# openstack-dashboard-ubuntu-theme package, or remove this file.
TEMPLATE_DIRS = ('/usr/share/openstack-dashboard-ubuntu-theme/templates', )

View File

@@ -0,0 +1,321 @@
/*
* mixins
*
* @section mixins
*/
@mixin font-size ($size: 16) {
font-size: ($size / $base)+em;
margin-bottom: (12 / $size)+em;
}
@mixin box-sizing ($type: border-box) {
-webkit-box-sizing: $type;
-moz-box-sizing: $type;
box-sizing: $type;
}
@mixin rounded-corners($radius: 4px 4px 4px 4px) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
@mixin box-shadow($shadow...) {
-moz-box-shadow: $shadow;
-webkit-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin gradient($from, $to) {
background-color: $to;
background-image: -moz-linear-gradient($from, $to);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from($from), to($to));
background-image: -webkit-linear-gradient($from, $to);
background-image: -o-linear-gradient($from, $to);
}
@mixin footer($background) {
padding: $gutter-width $two-col $gutter-width $four-col;
margin-bottom: 0;
background: url($background) no-repeat scroll $one-col center #F7F7F7;
}
@mixin clearfix() {
*zoom:1;
&:before,
&:after {
content:"";
display:table;
}
&:after {
clear:both;
}
}
/**
* standard colors
*
* @colordef standard colors
*/
/* assets database path */
$asset-path: "//assets.ubuntu.com/sites/ubuntu/latest/u/img/";
/* usage: background: url(#{$asset-path}/site/background.jpg) no-repeat 0 0; */
$ubuntu-orange: #dd4814; /* ubuntu orange (used for text links also on any site except canonical) */
$light-orange: #fdf6f2; /* used as background on pre text */
$canonical-aubergine: #772953; /* canonical aubergine */
$light-aubergine: #77216f; /* light aubergine (consumer) */
$mid-aubergine: #5e2750; /* mid aubergine (both) */
$dark-aubergine: #2c001e; /* dark aubergine (enterprise) */
$warm-grey: #888888; /* warm grey */
$cool-grey: #333333; /* cool grey */
$light-grey: #f7f7f7; /* light grey */
/* notifications */
$error: #df382c; /* red */
$warning: #eca918; /* yellow */
$success: #38b44a; /* green */
$information: #19b6ee; /* cyan */
/* colour coded status - from negative to positive (Icon: canonical circle) */
$status-red: #df382c; /* red, for status that require immediate attention */
$status-grey: #888888; /* grey, for disabled status or ones that dont require attention */
$status-yellow: #efb73e; /* yellow, for status that require attention */
$status-blue: #19b6ee; /* blue, for status that dont require action */
$status-green: #38b44a; /* green, for positive status */
/* misc colours */
$box-solid-grey: #efefef;
$link-color: $ubuntu-orange; /* This is the global link color, mainly used for links in content */
/* grid variables */
$base: 14;
$gutter-width: 20px;
$grid-gutter: 20px;
$gutter: 2.12766%;
$one-col: 6.38297%;
$two-col: 14.89361%;
$three-col: 23.40425%;
$four-col: 31.91489%;
$five-col: 40.42553%;
$six-col: 48.93617%;
$seven-col: 57.4468%;
$eight-col: 65.95744%;
$nine-col: 74.46808%;
$ten-col: 82.97872%;
$eleven-col: 91.48936%;
$nav-bg: #f0f0f0;
$nav-link-color: #333;
$nav-border-dark: #d4d7d4;
$nav-border-light: #f2f2f4;
$nav-hover-bg: #d0d0d0;
$nav-active-bg: #ddd;
@media only screen and (min-width : 768px) {
$base: 15;
}
@media only screen and (min-width: 984px) {
$base: 14;
}
/* Fonts */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 300;
src: url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/e7URzK__gdJcp1hLJYAEag.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/mZSs29ggGoaqrCNB3kDfZQ.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 500;
src: local('Ubuntu Medium'), local('Ubuntu-Medium'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/I5PmmDkYShUQg-ah7wh25w.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
src: local('Ubuntu Bold'), local('Ubuntu-Bold'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/trnbTfqisuuhRVI3i45C5w.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 300;
src: local('Ubuntu Light Italic'), local('Ubuntu-LightItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/j-TYDdXcC_eQzhhp386SjT8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 400;
src: local('Ubuntu Italic'), local('Ubuntu-Italic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/GZMdC02DTXXx8AdUvU2etw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 500;
src: local('Ubuntu Medium Italic'), local('Ubuntu-MediumItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/NWdMogIO7U6AtEM4dDdf_T8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 700;
src: local('Ubuntu Bold Italic'), local('Ubuntu-BoldItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/pqisLQoeO9YTDCNnlQ9bfz8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
body {
color: #333;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
}
/* Headings */
.page-header {
color: #333;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
border: none;
h1, h2 {
font-size: 2.8125em;
font-weight: 300;
line-height: 1.3;
margin-bottom: .5em;
}
}
.table_header h3 {
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
}
/* Links */
a,
a:link,
a:hover,
a:focus,
a:active {
color: $ubuntu-orange;
}
/* CTA Buttons */
.btn,
.btn:link {
@include box-sizing();
@include font-size (14);
@include rounded-corners(3px);
background: $ubuntu-orange;
border: none;
color: #fff;
text-decoration:none;
display: inline-block;
margin: 0;
padding: 8px 14px;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
-webkit-font-smoothing: subpixel-antialiased;
-moz-font-smoothing: subpixel-antialiased;
-o-font-smoothing: subpixel-antialiased;
font-smoothing: subpixel-antialiased;
padding: 4px 14px;
text-align: center;
}
.btn-primary.disabled,
.btn-primary.disabled:hover {
background-color: $ubuntu-orange;
}
a.link-cta-inverted,
button.cta-inverted {
background: #fff;
color: $cool-grey;
}
.btn:hover,
.btn:focus,
.btn:active,
{
color: #fff;
text-decoration: none;
background: #c03f11;
}
.btn.close,
.modal-footer .btn.close,
.btn.cancel,
.modal-footer .btn.cancel {
color: $cool-grey;
font-weight: 300;
background-color: #ccc;
}
.btn.close:hover,
.modal-footer .btn.close:hover,
.btn.cancel:hover,
.modal-footer .btn.cancel:hover {
text-decoration: none;
background-color: $warm-grey;
color: #fff;
text-shadow: none;
}
/* Login Screen */
#splash .login {
background-color: $ubuntu-orange;
background-image: url("../img/ubuntu.png");
padding-top: 100px;
div {
background-color: #fff;
}
}
/* Header / Menu */
.topbar {
background-color: $ubuntu-orange;
border: none;
-moz-box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
-webkit-box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
color: #fff;
height: 50px;
padding: 15px 0 0 0;
.dropdown-toggle:hover {
text-decoration: none;
}
}
.topbar .context-box .context-selection .dropdown-toggle {
background: none;
border: none;
padding: 0;
margin: 0;
color: #fff;
}
#profile_editor_switcher {
margin-top: -3px;
}
#user_info > a {
color: #fff;
}
h1.brand a {
background-image: url("../img/ubuntu.png");
margin-top: -5px;
min-width: 400px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,14 @@
{% load compress %}
{% compress css %}
<link href='{{ STATIC_URL }}dashboard/scss/horizon.scss' type='text/scss' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/css/rickshaw.css' type='text/css' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}horizon/lib/bootstrap_datepicker/datepicker3.css' type='text/css' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/scss/horizon_charts.scss' type='text/scss' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}dashboard/scss/horizon_workflow.scss' type='text/scss' media='screen' rel='stylesheet' />
<link href="{{ STATIC_URL }}horizon/lib/font-awesome/scss/font-awesome.scss" type="text/scss" media="screen" rel="stylesheet" />
<link href='{{ STATIC_URL }}ubuntu/css/ubuntu.scss' type='text/scss' media='screen' rel='stylesheet' />
{% endcompress %}
<link rel="shortcut icon" href="{{ STATIC_URL }}ubuntu/img/favicon-ubuntu.ico"/>

View File

@@ -0,0 +1,5 @@
# The presence of this file in /etc/openstack-dashboard/ and/or
# /usr/share/openstack-dashboard/openstack_dashboard/local/ will
# enable the Ubuntu theme for Horizon. To disable, remove the
# openstack-dashboard-ubuntu-theme package, or remove this file.
TEMPLATE_DIRS = ('/usr/share/openstack-dashboard-ubuntu-theme/templates', )

View File

@@ -0,0 +1,321 @@
/*
* mixins
*
* @section mixins
*/
@mixin font-size ($size: 16) {
font-size: ($size / $base)+em;
margin-bottom: (12 / $size)+em;
}
@mixin box-sizing ($type: border-box) {
-webkit-box-sizing: $type;
-moz-box-sizing: $type;
box-sizing: $type;
}
@mixin rounded-corners($radius: 4px 4px 4px 4px) {
-webkit-border-radius: $radius;
-moz-border-radius: $radius;
border-radius: $radius;
}
@mixin box-shadow($shadow...) {
-moz-box-shadow: $shadow;
-webkit-box-shadow: $shadow;
box-shadow: $shadow;
}
@mixin gradient($from, $to) {
background-color: $to;
background-image: -moz-linear-gradient($from, $to);
background-image: -webkit-gradient(linear, 0% 0%, 0% 100%, from($from), to($to));
background-image: -webkit-linear-gradient($from, $to);
background-image: -o-linear-gradient($from, $to);
}
@mixin footer($background) {
padding: $gutter-width $two-col $gutter-width $four-col;
margin-bottom: 0;
background: url($background) no-repeat scroll $one-col center #F7F7F7;
}
@mixin clearfix() {
*zoom:1;
&:before,
&:after {
content:"";
display:table;
}
&:after {
clear:both;
}
}
/**
* standard colors
*
* @colordef standard colors
*/
/* assets database path */
$asset-path: "//assets.ubuntu.com/sites/ubuntu/latest/u/img/";
/* usage: background: url(#{$asset-path}/site/background.jpg) no-repeat 0 0; */
$ubuntu-orange: #dd4814; /* ubuntu orange (used for text links also on any site except canonical) */
$light-orange: #fdf6f2; /* used as background on pre text */
$canonical-aubergine: #772953; /* canonical aubergine */
$light-aubergine: #77216f; /* light aubergine (consumer) */
$mid-aubergine: #5e2750; /* mid aubergine (both) */
$dark-aubergine: #2c001e; /* dark aubergine (enterprise) */
$warm-grey: #888888; /* warm grey */
$cool-grey: #333333; /* cool grey */
$light-grey: #f7f7f7; /* light grey */
/* notifications */
$error: #df382c; /* red */
$warning: #eca918; /* yellow */
$success: #38b44a; /* green */
$information: #19b6ee; /* cyan */
/* colour coded status - from negative to positive (Icon: canonical circle) */
$status-red: #df382c; /* red, for status that require immediate attention */
$status-grey: #888888; /* grey, for disabled status or ones that dont require attention */
$status-yellow: #efb73e; /* yellow, for status that require attention */
$status-blue: #19b6ee; /* blue, for status that dont require action */
$status-green: #38b44a; /* green, for positive status */
/* misc colours */
$box-solid-grey: #efefef;
$link-color: $ubuntu-orange; /* This is the global link color, mainly used for links in content */
/* grid variables */
$base: 14;
$gutter-width: 20px;
$grid-gutter: 20px;
$gutter: 2.12766%;
$one-col: 6.38297%;
$two-col: 14.89361%;
$three-col: 23.40425%;
$four-col: 31.91489%;
$five-col: 40.42553%;
$six-col: 48.93617%;
$seven-col: 57.4468%;
$eight-col: 65.95744%;
$nine-col: 74.46808%;
$ten-col: 82.97872%;
$eleven-col: 91.48936%;
$nav-bg: #f0f0f0;
$nav-link-color: #333;
$nav-border-dark: #d4d7d4;
$nav-border-light: #f2f2f4;
$nav-hover-bg: #d0d0d0;
$nav-active-bg: #ddd;
@media only screen and (min-width : 768px) {
$base: 15;
}
@media only screen and (min-width: 984px) {
$base: 14;
}
/* Fonts */
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 300;
src: url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/e7URzK__gdJcp1hLJYAEag.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 400;
src: local('Ubuntu'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/mZSs29ggGoaqrCNB3kDfZQ.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 500;
src: local('Ubuntu Medium'), local('Ubuntu-Medium'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/I5PmmDkYShUQg-ah7wh25w.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: normal;
font-weight: 700;
src: local('Ubuntu Bold'), local('Ubuntu-Bold'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/trnbTfqisuuhRVI3i45C5w.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 300;
src: local('Ubuntu Light Italic'), local('Ubuntu-LightItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/j-TYDdXcC_eQzhhp386SjT8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 400;
src: local('Ubuntu Italic'), local('Ubuntu-Italic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/GZMdC02DTXXx8AdUvU2etw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 500;
src: local('Ubuntu Medium Italic'), local('Ubuntu-MediumItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/NWdMogIO7U6AtEM4dDdf_T8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
@font-face {
font-family: 'Ubuntu';
font-style: italic;
font-weight: 700;
src: local('Ubuntu Bold Italic'), local('Ubuntu-BoldItalic'), url('http://themes.googleusercontent.com/static/fonts/ubuntu/v5/pqisLQoeO9YTDCNnlQ9bfz8E0i7KZn-EPnyo3HZu7kw.woff') format('woff');
}
body {
color: #333;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
}
/* Headings */
.page-header {
color: #333;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
border: none;
h1, h2 {
font-size: 2.8125em;
font-weight: 300;
line-height: 1.3;
margin-bottom: .5em;
}
}
.table_header h3 {
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
}
/* Links */
a,
a:link,
a:hover,
a:focus,
a:active {
color: $ubuntu-orange;
}
/* CTA Buttons */
.btn,
.btn:link {
@include box-sizing();
@include font-size (14);
@include rounded-corners(3px);
background: $ubuntu-orange;
border: none;
color: #fff;
text-decoration:none;
display: inline-block;
margin: 0;
padding: 8px 14px;
font-family: Ubuntu, Arial, "libra sans", sans-serif;
font-weight: 300;
-webkit-font-smoothing: subpixel-antialiased;
-moz-font-smoothing: subpixel-antialiased;
-o-font-smoothing: subpixel-antialiased;
font-smoothing: subpixel-antialiased;
padding: 4px 14px;
text-align: center;
}
.btn-primary.disabled,
.btn-primary.disabled:hover {
background-color: $ubuntu-orange;
}
a.link-cta-inverted,
button.cta-inverted {
background: #fff;
color: $cool-grey;
}
.btn:hover,
.btn:focus,
.btn:active,
{
color: #fff;
text-decoration: none;
background: #c03f11;
}
.btn.close,
.modal-footer .btn.close,
.btn.cancel,
.modal-footer .btn.cancel {
color: $cool-grey;
font-weight: 300;
background-color: #ccc;
}
.btn.close:hover,
.modal-footer .btn.close:hover,
.btn.cancel:hover,
.modal-footer .btn.cancel:hover {
text-decoration: none;
background-color: $warm-grey;
color: #fff;
text-shadow: none;
}
/* Login Screen */
#splash .login {
background-color: $ubuntu-orange;
background-image: url("../img/ubuntu.png");
padding-top: 100px;
div {
background-color: #fff;
}
}
/* Header / Menu */
.topbar {
background-color: $ubuntu-orange;
border: none;
-moz-box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
-webkit-box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
box-shadow: 0 1px 2px 1px rgba(0,0,0,0.2);
color: #fff;
height: 50px;
padding: 15px 0 0 0;
.dropdown-toggle:hover {
text-decoration: none;
}
}
.topbar .context-box .context-selection .dropdown-toggle {
background: none;
border: none;
padding: 0;
margin: 0;
color: #fff;
}
#profile_editor_switcher {
margin-top: -3px;
}
#user_info > a {
color: #fff;
}
h1.brand a {
background-image: url("../img/ubuntu.png");
margin-top: -5px;
min-width: 400px;
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 623 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

View File

@@ -0,0 +1,9 @@
{% load compress %}
{% compress css %}
<link href='{{ STATIC_URL }}dashboard/scss/horizon.scss' type='text/scss' media='screen' rel='stylesheet' />
<link href='{{ STATIC_URL }}ubuntu/css/ubuntu.scss' type='text/scss' media='screen' rel='stylesheet' />
{% endcompress %}
<link rel="shortcut icon" href="{{ STATIC_URL }}ubuntu/img/favicon-ubuntu.ico"/>

View File

@@ -0,0 +1,5 @@
# The presence of this file in /etc/openstack-dashboard/ and/or
# /usr/share/openstack-dashboard/openstack_dashboard/local/ will
# enable the Ubuntu theme for Horizon. To disable, remove the
# openstack-dashboard-ubuntu-theme package, or remove this file.
TEMPLATE_DIRS = ('/usr/share/openstack-dashboard-ubuntu-theme/templates', )

View File

@@ -0,0 +1,9 @@
#!/usr/bin/python
"""Amulet tests on a basic openstack-dashboard git deployment on trusty-icehouse."""
from basic_deployment import OpenstackDashboardBasicDeployment
if __name__ == '__main__':
deployment = OpenstackDashboardBasicDeployment(series='trusty', git=True)
deployment.run_tests()

12
tests/18-basic-trusty-juno-git Executable file
View File

@@ -0,0 +1,12 @@
#!/usr/bin/python
"""Amulet tests on a basic openstack-dashboard git deployment on trusty-juno."""
from basic_deployment import OpenstackDashboardBasicDeployment
if __name__ == '__main__':
deployment = OpenstackDashboardBasicDeployment(series='trusty',
openstack='cloud:trusty-juno',
source='cloud:trusty-updates/juno',
git=True)
deployment.run_tests()

View File

@@ -3,6 +3,7 @@
import amulet
import time
import urllib2
import yaml
from charmhelpers.contrib.openstack.amulet.deployment import (
OpenStackAmuletDeployment
@@ -21,10 +22,12 @@ u = OpenStackAmuletUtils(ERROR)
class OpenstackDashboardBasicDeployment(OpenStackAmuletDeployment):
"""Amulet tests on a basic openstack-dashboard deployment."""
def __init__(self, series, openstack=None, source=None, stable=False):
def __init__(self, series, openstack=None, source=None, git=False,
stable=False):
"""Deploy the entire test environment."""
super(OpenstackDashboardBasicDeployment, self).__init__(series, openstack,
source, stable)
self.git = git
self._add_services()
self._add_relations()
self._configure_services()
@@ -39,7 +42,7 @@ class OpenstackDashboardBasicDeployment(OpenStackAmuletDeployment):
compatible with the local charm (e.g. stable or next).
"""
this_service = {'name': 'openstack-dashboard'}
other_services = [{'name': 'keystone'}]
other_services = [{'name': 'keystone'}, {'name': 'mysql'}]
super(OpenstackDashboardBasicDeployment, self)._add_services(this_service,
other_services)
@@ -47,14 +50,36 @@ class OpenstackDashboardBasicDeployment(OpenStackAmuletDeployment):
"""Add all of the relations for the services."""
relations = {
'openstack-dashboard:identity-service': 'keystone:identity-service',
'keystone:shared-db': 'mysql:shared-db',
}
super(OpenstackDashboardBasicDeployment, self)._add_relations(relations)
def _configure_services(self):
"""Configure all of the services."""
horizon_config = {}
if self.git:
branch = 'stable/' + self._get_openstack_release_string()
openstack_origin_git = {
'repositories': [
{'name': 'requirements',
'repository': 'git://git.openstack.org/openstack/requirements',
'branch': branch},
{'name': 'horizon',
'repository': 'git://git.openstack.org/openstack/horizon',
'branch': branch},
],
'directory': '/mnt/openstack-git',
'http_proxy': 'http://squid.internal:3128',
'https_proxy': 'https://squid.internal:3128',
}
horizon_config['openstack-origin-git'] = yaml.dump(openstack_origin_git)
keystone_config = {'admin-password': 'openstack',
'admin-token': 'ubuntutesting'}
configs = {'keystone': keystone_config}
mysql_config = {'dataset-size': '50%'}
configs = {'openstack-dashboard': horizon_config,
'mysql': mysql_config,
'keystone': keystone_config}
super(OpenstackDashboardBasicDeployment, self)._configure_services(configs)
def _initialize_tests(self):

View File

@@ -15,6 +15,7 @@
# along with charm-helpers. If not, see <http://www.gnu.org/licenses/>.
import six
from collections import OrderedDict
from charmhelpers.contrib.amulet.deployment import (
AmuletDeployment
)
@@ -100,12 +101,34 @@ class OpenStackAmuletDeployment(AmuletDeployment):
"""
(self.precise_essex, self.precise_folsom, self.precise_grizzly,
self.precise_havana, self.precise_icehouse,
self.trusty_icehouse) = range(6)
self.trusty_icehouse, self.trusty_juno, self.trusty_kilo) = range(8)
releases = {
('precise', None): self.precise_essex,
('precise', 'cloud:precise-folsom'): self.precise_folsom,
('precise', 'cloud:precise-grizzly'): self.precise_grizzly,
('precise', 'cloud:precise-havana'): self.precise_havana,
('precise', 'cloud:precise-icehouse'): self.precise_icehouse,
('trusty', None): self.trusty_icehouse}
('trusty', None): self.trusty_icehouse,
('trusty', 'cloud:trusty-juno'): self.trusty_juno,
('trusty', 'cloud:trusty-kilo'): self.trusty_kilo}
return releases[(self.series, self.openstack)]
def _get_openstack_release_string(self):
"""Get openstack release string.
Return a string representing the openstack release.
"""
releases = OrderedDict([
('precise', 'essex'),
('quantal', 'folsom'),
('raring', 'grizzly'),
('saucy', 'havana'),
('trusty', 'icehouse'),
('utopic', 'juno'),
('vivid', 'kilo'),
])
if self.openstack:
os_origin = self.openstack.split(':')[1]
return os_origin.split('%s-' % self.series)[1].split('/')[0]
else:
return releases[self.series]

View File

@@ -1,2 +1,4 @@
import sys
sys.path.append('actions')
sys.path.append('hooks')

View File

@@ -0,0 +1,88 @@
from mock import patch
import os
os.environ['JUJU_UNIT_NAME'] = 'horizon'
with patch('horizon_utils.register_configs') as register_configs:
import git_reinstall
from test_utils import (
CharmTestCase
)
TO_PATCH = [
'config',
]
openstack_origin_git = \
"""repositories:
- {name: requirements,
repository: 'git://git.openstack.org/openstack/requirements',
branch: stable/juno}
- {name: horizon,
repository: 'git://git.openstack.org/openstack/horizon',
branch: stable/juno}"""
class TestHorizonActions(CharmTestCase):
def setUp(self):
super(TestHorizonActions, self).setUp(git_reinstall, TO_PATCH)
self.config.side_effect = self.test_config.get
@patch.object(git_reinstall, 'action_set')
@patch.object(git_reinstall, 'action_fail')
@patch.object(git_reinstall, 'git_install')
@patch('charmhelpers.contrib.openstack.utils.config')
def test_git_reinstall(self, _config, git_install, action_fail,
action_set):
_config.return_value = openstack_origin_git
self.test_config.set('openstack-origin-git', openstack_origin_git)
git_reinstall.git_reinstall()
git_install.assert_called_with(openstack_origin_git)
self.assertTrue(git_install.called)
self.assertFalse(action_set.called)
self.assertFalse(action_fail.called)
@patch.object(git_reinstall, 'action_set')
@patch.object(git_reinstall, 'action_fail')
@patch.object(git_reinstall, 'git_install')
@patch('charmhelpers.contrib.openstack.utils.config')
def test_git_reinstall_not_configured(self, config, git_install,
action_fail, action_set):
config.return_value = None
git_reinstall.git_reinstall()
msg = 'openstack-origin-git is not configured'
action_fail.assert_called_with(msg)
self.assertFalse(git_install.called)
self.assertFalse(action_set.called)
@patch.object(git_reinstall, 'action_set')
@patch.object(git_reinstall, 'action_fail')
@patch.object(git_reinstall, 'git_install')
@patch('charmhelpers.contrib.openstack.utils.config')
def test_git_reinstall_exception(self, _config, git_install, action_fail,
action_set):
_config.return_value = openstack_origin_git
e = OSError('something bad happened')
git_install.side_effect = e
traceback = (
"Traceback (most recent call last):\n"
" File \"actions/git_reinstall.py\", line 37, in git_reinstall\n"
" git_install(config(\'openstack-origin-git\'))\n"
" File \"/usr/lib/python2.7/dist-packages/mock.py\", line 964, in __call__\n" # noqa
" return _mock_self._mock_call(*args, **kwargs)\n"
" File \"/usr/lib/python2.7/dist-packages/mock.py\", line 1019, in _mock_call\n" # noqa
" raise effect\n"
"OSError: something bad happened\n")
git_reinstall.git_reinstall()
msg = 'git-reinstall resulted in an unexpected error'
action_fail.assert_called_with(msg)
action_set.assert_called_with({'traceback': traceback})

View File

@@ -1,4 +1,5 @@
from mock import MagicMock, patch, call
import yaml
import horizon_utils as utils
_register_configs = utils.register_configs
utils.register_configs = MagicMock()
@@ -32,6 +33,8 @@ TO_PATCH = [
'os_release',
'get_iface_for_address',
'get_netmask_for_address',
'git_install',
'git_post_install_late',
'update_nrpe_config',
'lsb_release',
]
@@ -53,7 +56,9 @@ class TestHorizonHooks(CharmTestCase):
hooks.hooks.execute([
'hooks/{}'.format(hookname)])
def test_install_hook(self):
@patch.object(utils, 'git_install_requested')
def test_install_hook(self, _git_requested):
_git_requested.return_value = False
self.filter_installed_packages.return_value = ['foo', 'bar']
self.os_release.return_value = 'icehouse'
self._call_hook('install')
@@ -61,7 +66,9 @@ class TestHorizonHooks(CharmTestCase):
self.apt_update.assert_called_with(fatal=True)
self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
def test_install_hook_precise(self):
@patch.object(utils, 'git_install_requested')
def test_install_hook_precise(self, _git_requested):
_git_requested.return_value = False
self.filter_installed_packages.return_value = ['foo', 'bar']
self.os_release.return_value = 'icehouse'
self.lsb_release.return_value = {'DISTRIB_CODENAME': 'precise'}
@@ -74,7 +81,9 @@ class TestHorizonHooks(CharmTestCase):
]
self.apt_install.assert_has_calls(calls)
def test_install_hook_icehouse_pkgs(self):
@patch.object(utils, 'git_install_requested')
def test_install_hook_icehouse_pkgs(self, _git_requested):
_git_requested.return_value = False
self.os_release.return_value = 'icehouse'
self._call_hook('install')
for pkg in ['nodejs', 'node-less']:
@@ -83,7 +92,9 @@ class TestHorizonHooks(CharmTestCase):
)
self.apt_install.assert_called()
def test_install_hook_pre_icehouse_pkgs(self):
@patch.object(utils, 'git_install_requested')
def test_install_hook_pre_icehouse_pkgs(self, _git_requested):
_git_requested.return_value = False
self.os_release.return_value = 'grizzly'
self._call_hook('install')
for pkg in ['nodejs', 'node-less']:
@@ -92,9 +103,37 @@ class TestHorizonHooks(CharmTestCase):
)
self.apt_install.assert_called()
@patch.object(utils, 'git_install_requested')
def test_install_hook_git(self, _git_requested):
_git_requested.return_value = True
self.filter_installed_packages.return_value = ['foo', 'bar']
repo = 'cloud:trusty-juno'
openstack_origin_git = {
'repositories': [
{'name': 'requirements',
'repository': 'git://git.openstack.org/openstack/requirements', # noqa
'branch': 'stable/juno'},
{'name': 'horizon',
'repository': 'git://git.openstack.org/openstack/horizon',
'branch': 'stable/juno'}
],
'directory': '/mnt/openstack-git',
}
projects_yaml = yaml.dump(openstack_origin_git)
self.test_config.set('openstack-origin', repo)
self.test_config.set('openstack-origin-git', projects_yaml)
self._call_hook('install')
self.assertTrue(self.execd_preinstall.called)
self.configure_installation_source.assert_called_with(repo)
self.apt_update.assert_called_with(fatal=True)
self.apt_install.assert_called_with(['foo', 'bar'], fatal=True)
self.git_install.assert_called_with(projects_yaml)
@patch('charmhelpers.core.host.file_hash')
@patch('charmhelpers.core.host.service')
def test_upgrade_charm_hook(self, _service, _hash):
@patch.object(utils, 'git_install_requested')
def test_upgrade_charm_hook(self, _git_requested, _service, _hash):
_git_requested.return_value = False
side_effects = []
[side_effects.append(None) for f in RESTART_MAP.keys()]
[side_effects.append('bar') for f in RESTART_MAP.keys()]
@@ -175,7 +214,9 @@ class TestHorizonHooks(CharmTestCase):
'ha-relation-joined')
@patch('horizon_hooks.keystone_joined')
def test_config_changed_no_upgrade(self, _joined):
@patch.object(hooks, 'git_install_requested')
def test_config_changed_no_upgrade(self, _git_requested, _joined):
_git_requested.return_value = False
self.relation_ids.return_value = ['identity/0']
self.openstack_upgrade_available.return_value = False
self._call_hook('config-changed')
@@ -189,13 +230,39 @@ class TestHorizonHooks(CharmTestCase):
self.CONFIGS.write_all.assert_called()
self.open_port.assert_has_calls([call(80), call(443)])
def test_config_changed_do_upgrade(self):
@patch.object(hooks, 'git_install_requested')
def test_config_changed_do_upgrade(self, _git_requested):
_git_requested.return_value = False
self.relation_ids.return_value = []
self.test_config.set('openstack-origin', 'cloud:precise-grizzly')
self.openstack_upgrade_available.return_value = True
self._call_hook('config-changed')
self.do_openstack_upgrade.assert_called()
@patch.object(hooks, 'git_install_requested')
@patch.object(hooks, 'config_value_changed')
def test_config_changed_git_updated(self, _config_val_changed,
_git_requested):
_git_requested.return_value = True
repo = 'cloud:trusty-juno'
openstack_origin_git = {
'repositories': [
{'name': 'requirements',
'repository': 'git://git.openstack.org/openstack/requirements', # noqa
'branch': 'stable/juno'},
{'name': 'horizon',
'repository': 'git://git.openstack.org/openstack/horizon',
'branch': 'stable/juno'}
],
'directory': '/mnt/openstack-git',
}
projects_yaml = yaml.dump(openstack_origin_git)
self.test_config.set('openstack-origin', repo)
self.test_config.set('openstack-origin-git', projects_yaml)
self._call_hook('config-changed')
self.git_install.assert_called_with(projects_yaml)
self.assertFalse(self.do_openstack_upgrade.called)
def test_keystone_joined_in_relation(self):
self._call_hook('identity-service-relation-joined')
self.relation_set.assert_called_with(

View File

@@ -1,4 +1,5 @@
from mock import MagicMock, patch, call
import os
from collections import OrderedDict
import charmhelpers.contrib.openstack.templating as templating
templating.OSConfigRenderer = MagicMock()
@@ -9,7 +10,6 @@ from test_utils import (
)
TO_PATCH = [
'get_os_codename_package',
'config',
'get_os_codename_install_source',
'apt_update',
@@ -17,13 +17,23 @@ TO_PATCH = [
'configure_installation_source',
'log',
'cmp_pkgrevno',
'os_release',
]
openstack_origin_git = \
"""repositories:
- {name: requirements,
repository: 'git://git.openstack.org/openstack/requirements',
branch: stable/juno}
- {name: horizon,
repository: 'git://git.openstack.org/openstack/horizon',
branch: stable/juno}"""
class TestHorizonUtils(CharmTestCase):
class TestHorizohorizon_utils(CharmTestCase):
def setUp(self):
super(TestHorizonUtils, self).setUp(horizon_utils, TO_PATCH)
super(TestHorizohorizon_utils, self).setUp(horizon_utils, TO_PATCH)
@patch('subprocess.call')
def test_enable_ssl(self, _call):
@@ -71,7 +81,7 @@ class TestHorizonUtils(CharmTestCase):
@patch('os.path.isdir')
def test_register_configs(self, _isdir):
_isdir.return_value = True
self.get_os_codename_package.return_value = 'havana'
self.os_release.return_value = 'havana'
self.cmp_pkgrevno.return_value = -1
configs = horizon_utils.register_configs()
confs = [horizon_utils.LOCAL_SETTINGS,
@@ -93,7 +103,7 @@ class TestHorizonUtils(CharmTestCase):
def test_register_configs_apache24(self, _isdir, _isfile, _remove):
_isdir.return_value = True
_isfile.return_value = True
self.get_os_codename_package.return_value = 'havana'
self.os_release.return_value = 'havana'
self.cmp_pkgrevno.return_value = 1
configs = horizon_utils.register_configs()
confs = [horizon_utils.LOCAL_SETTINGS,
@@ -118,7 +128,7 @@ class TestHorizonUtils(CharmTestCase):
@patch('os.path.isdir')
def test_register_configs_pre_install(self, _isdir):
_isdir.return_value = False
self.get_os_codename_package.return_value = None
self.os_release.return_value = None
configs = horizon_utils.register_configs()
confs = [horizon_utils.LOCAL_SETTINGS,
horizon_utils.HAPROXY_CONF,
@@ -131,3 +141,161 @@ class TestHorizonUtils(CharmTestCase):
calls.append(
call(conf, horizon_utils.CONFIG_FILES[conf]['hook_contexts']))
configs.register.assert_has_calls(calls)
@patch.object(horizon_utils, 'git_install_requested')
@patch.object(horizon_utils, 'git_clone_and_install')
@patch.object(horizon_utils, 'git_post_install')
@patch.object(horizon_utils, 'git_pre_install')
def test_git_install(self, git_pre, git_post, git_clone_and_install,
git_requested):
projects_yaml = openstack_origin_git
git_requested.return_value = True
horizon_utils.git_install(projects_yaml)
self.assertTrue(git_pre.called)
git_clone_and_install.assert_called_with(openstack_origin_git,
core_project='horizon')
self.assertTrue(git_post.called)
@patch.object(horizon_utils, 'mkdir')
@patch.object(horizon_utils, 'add_user_to_group')
@patch.object(horizon_utils, 'add_group')
@patch.object(horizon_utils, 'adduser')
@patch('subprocess.check_call')
def test_git_pre_install(self, check_call, adduser, add_group,
add_user_to_group, mkdir):
horizon_utils.git_pre_install()
adduser.assert_called_with('horizon', shell='/bin/bash',
system_user=True)
check_call.assert_called_with(['usermod', '--home',
'/usr/share/openstack-dashboard/',
'horizon'])
add_group.assert_called_with('horizon', system_group=True)
add_user_to_group.assert_called_with('horizon', 'horizon')
them_dir = '/usr/share/openstack-dashboard-ubuntu-theme'
expected = [
call('/etc/openstack-dashboard', owner='root',
group='root', perms=0755, force=False),
call('/usr/share/openstack-dashboard', owner='root',
group='root', perms=0755, force=False),
call('/usr/share/openstack-dashboard/bin/less', owner='root',
group='root', perms=0755, force=False),
call(os.path.join(them_dir, 'static/ubuntu/css'),
owner='root', group='root', perms=0755, force=False),
call(os.path.join(them_dir, 'static/ubuntu/img'),
owner='root', group='root', perms=0755, force=False),
call(os.path.join(them_dir, 'templates'),
owner='root', group='root', perms=0755, force=False),
call('/var/lib/openstack-dashboard', owner='horizon',
group='horizon', perms=0700, force=False),
]
self.assertEquals(mkdir.call_args_list, expected)
@patch.object(horizon_utils, 'git_src_dir')
@patch.object(horizon_utils, 'service_restart')
@patch.object(horizon_utils, 'charm_dir')
@patch('shutil.copyfile')
@patch('shutil.copytree')
@patch('os.path.join')
@patch('os.path.exists')
@patch('os.symlink')
@patch('os.chmod')
@patch('os.chown')
@patch('os.lchown')
@patch('os.walk')
@patch('subprocess.check_call')
@patch('pwd.getpwnam')
@patch('grp.getgrnam')
def test_git_post_install(self, grnam, pwnam, check_call, walk, lchown,
chown, chmod, symlink, exists, join, copytree,
copyfile, charm_dir, service_restart,
git_src_dir):
class IDs(object):
pw_uid = 999
gr_gid = 999
pwnam.return_value = IDs
grnam.return_value = IDs
projects_yaml = openstack_origin_git
join.return_value = 'joined-string'
walk.return_value = yield '/root', ['dir'], ['file']
exists.return_value = False
horizon_utils.git_post_install(projects_yaml)
expected = [
call('joined-string',
'/usr/share/openstack-dashboard/manage.py'),
call('joined-string',
'/usr/share/openstack-dashboard/settings.py'),
call('joined-string',
'/etc/openstack-dashboard/local_settings.py'),
call('joined-string',
'/etc/apache2/conf-available/openstack-dashboard.conf'),
call('joined-string',
'/etc/openstack-dashboard/ubuntu_theme.py'),
]
copyfile.assert_has_calls(expected, any_order=True)
expected = [
call('joined-string',
'/usr/share/openstack-dashboard/openstack_dashboard'),
call('joined-string', 'joined-string'),
call('joined-string', 'joined-string'),
call('joined-string', 'joined-string'),
]
copytree.assert_has_calls(expected)
expected = [
call('/usr/share/openstack-dashboard/static'),
call('joined-string'),
call('joined-string'),
call('joined-string'),
call('joined-string'),
]
exists.assert_has_calls(expected, any_order=True)
dist_pkgs_dir = '/usr/local/lib/python2.7/dist-packages'
expected = [
call('/usr/share/openstack-dashboard/openstack_dashboard/static',
'/usr/share/openstack-dashboard/static'),
call('/etc/openstack-dashboard/ubuntu_theme.py', 'joined-string'),
call('/usr/share/openstack-dashboard-ubuntu-theme/static/ubuntu',
'joined-string'),
call('/etc/openstack-dashboard/local_settings.py',
'joined-string'),
call(os.path.join(dist_pkgs_dir, 'horizon/static/horizon/'),
'joined-string'),
]
symlink.assert_has_calls(expected, any_order=True)
expected = [
call('/var/lib/openstack-dashboard', 0o750),
call('/share/openstack-dashboard/manage.py', 0o755),
]
chmod.assert_has_calls(expected)
expected = [
call(['/usr/share/openstack-dashboard/manage.py',
'collectstatic', '--noinput']),
call(['/usr/share/openstack-dashboard/manage.py',
'compress', '--force']),
call(['a2enconf', 'openstack-dashboard']),
]
check_call.assert_has_calls(expected)
expected = [
call('horizon'),
]
pwnam.assert_has_calls(expected)
grnam.assert_has_calls(expected)
expected = [
call('/etc/openstack-dashboard', 999, 999),
call('/usr/share/openstack-dashboard/openstack_dashboard/static',
999, 999),
call('/var/lib/openstack-dashboard', 999, 999),
]
chown.assert_has_calls(expected)
expected = [
call('/share/openstack-dashboard/openstack_dashboard/static'),
]
walk.assert_has_calls(expected)
expected = [
call('/root/dir', 999, 999),
call('/root/file', 999, 999),
]
lchown.assert_has_calls(expected)
expected = [
call('apache2'),
]
self.assertEquals(service_restart.call_args_list, expected)