Deploy from source
@@ -1,2 +1,3 @@
|
||||
.coverage
|
||||
bin
|
||||
tags
|
||||
|
||||
7
Makefile
@@ -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
@@ -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
@@ -0,0 +1,2 @@
|
||||
git-reinstall:
|
||||
description: Reinstall openstack-dashboard from the openstack-origin-git repositories.
|
||||
1
actions/git-reinstall
Symbolic link
@@ -0,0 +1 @@
|
||||
git_reinstall.py
|
||||
45
actions/git_reinstall.py
Executable 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()
|
||||
16
config.yaml
@@ -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
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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 {}
|
||||
|
||||
@@ -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
|
||||
|
||||
13
hooks/charmhelpers/contrib/openstack/templates/git.upstart
Normal 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 }}
|
||||
@@ -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 -%}
|
||||
@@ -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 -%}
|
||||
@@ -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 -%}
|
||||
@@ -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
|
||||
|
||||
@@ -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])
|
||||
|
||||
@@ -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))
|
||||
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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):
|
||||
|
||||
@@ -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
@@ -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>
|
||||
6778
templates/icehouse/theme/css/ubuntu.css
Normal file
BIN
templates/icehouse/theme/img/dots.png
Normal file
|
After Width: | Height: | Size: 163 B |
BIN
templates/icehouse/theme/img/favicon-ubuntu.ico
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
templates/icehouse/theme/img/header_stripes.png
Normal file
|
After Width: | Height: | Size: 474 B |
BIN
templates/icehouse/theme/img/juju.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
BIN
templates/icehouse/theme/img/ubuntu.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
12
templates/icehouse/theme/templates/_stylesheets.html
Normal 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"/>
|
||||
5
templates/icehouse/theme/ubuntu_theme.py
Normal 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', )
|
||||
321
templates/juno/theme/css/ubuntu.scss
Normal 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 don’t require attention */
|
||||
$status-yellow: #efb73e; /* yellow, for status that require attention */
|
||||
$status-blue: #19b6ee; /* blue, for status that don’t 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;
|
||||
}
|
||||
|
||||
BIN
templates/juno/theme/img/favicon-ubuntu.ico
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
templates/juno/theme/img/ubuntu.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
14
templates/juno/theme/templates/_stylesheets.html
Normal 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"/>
|
||||
5
templates/juno/theme/ubuntu_theme.py
Normal 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', )
|
||||
321
templates/kilo/theme/css/ubuntu.scss
Normal 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 don’t require attention */
|
||||
$status-yellow: #efb73e; /* yellow, for status that require attention */
|
||||
$status-blue: #19b6ee; /* blue, for status that don’t 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;
|
||||
}
|
||||
|
||||
BIN
templates/kilo/theme/img/favicon-ubuntu.ico
Normal file
|
After Width: | Height: | Size: 623 B |
BIN
templates/kilo/theme/img/ubuntu.png
Normal file
|
After Width: | Height: | Size: 4.1 KiB |
9
templates/kilo/theme/templates/_stylesheets.html
Normal 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"/>
|
||||
5
templates/kilo/theme/ubuntu_theme.py
Normal 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', )
|
||||
9
tests/16-basic-trusty-icehouse-git
Executable 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
@@ -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()
|
||||
@@ -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):
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -1,2 +1,4 @@
|
||||
import sys
|
||||
|
||||
sys.path.append('actions')
|
||||
sys.path.append('hooks')
|
||||
|
||||
88
unit_tests/test_actions_git_reinstall.py
Normal 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})
|
||||
@@ -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(
|
||||
|
||||
@@ -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)
|
||||
|
||||