summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorCorey Bryant <corey.bryant@canonical.com>2019-01-08 15:17:50 +0000
committerCorey Bryant <corey.bryant@canonical.com>2019-01-08 15:17:54 +0000
commitc4bc780f6028c880947f555a55832f652bce0596 (patch)
treee712b596f79f20403e13e837bc0246def5a1fadf
parentbb95b67df6057d42a1a23a4e9c6be94243d1c01e (diff)
downloadcharm-nova-lxd-c4bc780f6028c880947f555a55832f652bce0596.zip
charm-nova-lxd-c4bc780f6028c880947f555a55832f652bce0596.tar.gz
charm-nova-lxd-c4bc780f6028c880947f555a55832f652bce0596.tar.bz2
Sync charm-helpers
Change-Id: I33aea91461d6122d1c45b147544c4ab3247b21a7
-rw-r--r--hooks/charmhelpers/contrib/charmsupport/nrpe.py25
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/deployment.py5
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/utils.py3
-rw-r--r--hooks/charmhelpers/contrib/openstack/cert_utils.py2
-rw-r--r--hooks/charmhelpers/contrib/openstack/context.py8
-rw-r--r--hooks/charmhelpers/contrib/openstack/ha/utils.py133
-rw-r--r--hooks/charmhelpers/contrib/openstack/utils.py113
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/loopback.py6
-rw-r--r--hooks/charmhelpers/fetch/ubuntu.py8
9 files changed, 224 insertions, 79 deletions
diff --git a/hooks/charmhelpers/contrib/charmsupport/nrpe.py b/hooks/charmhelpers/contrib/charmsupport/nrpe.py
index e3d10c1..f59fdd6 100644
--- a/hooks/charmhelpers/contrib/charmsupport/nrpe.py
+++ b/hooks/charmhelpers/contrib/charmsupport/nrpe.py
@@ -305,7 +305,7 @@ class NRPE(object):
# update-status hooks are configured to firing every 5 minutes by
# default. When nagios-nrpe-server is restarted, the nagios server
- # reports checks failing causing unneccessary alerts. Let's not restart
+ # reports checks failing causing unnecessary alerts. Let's not restart
# on update-status hooks.
if not hook_name() == 'update-status':
service('restart', 'nagios-nrpe-server')
@@ -416,15 +416,20 @@ def copy_nrpe_checks(nrpe_files_dir=None):
"""
NAGIOS_PLUGINS = '/usr/local/lib/nagios/plugins'
- default_nrpe_files_dir = os.path.join(
- os.getenv('CHARM_DIR'),
- 'hooks',
- 'charmhelpers',
- 'contrib',
- 'openstack',
- 'files')
- if not nrpe_files_dir:
- nrpe_files_dir = default_nrpe_files_dir
+ if nrpe_files_dir is None:
+ # determine if "charmhelpers" is in CHARMDIR or CHARMDIR/hooks
+ for segment in ['.', 'hooks']:
+ nrpe_files_dir = os.path.abspath(os.path.join(
+ os.getenv('CHARM_DIR'),
+ segment,
+ 'charmhelpers',
+ 'contrib',
+ 'openstack',
+ 'files'))
+ if os.path.isdir(nrpe_files_dir):
+ break
+ else:
+ raise RuntimeError("Couldn't find charmhelpers directory")
if not os.path.exists(NAGIOS_PLUGINS):
os.makedirs(NAGIOS_PLUGINS)
for fname in glob.glob(os.path.join(nrpe_files_dir, "check_*")):
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index 1c96752..d1270a7 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -168,7 +168,8 @@ class OpenStackAmuletDeployment(AmuletDeployment):
'nrpe', 'openvswitch-odl', 'neutron-api-odl',
'odl-controller', 'cinder-backup', 'nexentaedge-data',
'nexentaedge-iscsi-gw', 'nexentaedge-swift-gw',
- 'cinder-nexentaedge', 'nexentaedge-mgmt']))
+ 'cinder-nexentaedge', 'nexentaedge-mgmt',
+ 'ceilometer-agent']))
if self.openstack:
for svc in services:
@@ -292,7 +293,9 @@ class OpenStackAmuletDeployment(AmuletDeployment):
('artful', None): self.artful_pike,
('bionic', None): self.bionic_queens,
('bionic', 'cloud:bionic-rocky'): self.bionic_rocky,
+ ('bionic', 'cloud:bionic-stein'): self.bionic_stein,
('cosmic', None): self.cosmic_rocky,
+ ('disco', None): self.disco_stein,
}
return releases[(self.series, self.openstack)]
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/utils.py b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
index 9133e9b..ea1fd8f 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/utils.py
@@ -57,7 +57,8 @@ OPENSTACK_RELEASES_PAIRS = [
'trusty_mitaka', 'xenial_mitaka', 'xenial_newton',
'yakkety_newton', 'xenial_ocata', 'zesty_ocata',
'xenial_pike', 'artful_pike', 'xenial_queens',
- 'bionic_queens', 'bionic_rocky', 'cosmic_rocky']
+ 'bionic_queens', 'bionic_rocky', 'cosmic_rocky',
+ 'bionic_stein', 'disco_stein']
class OpenStackAmuletUtils(AmuletUtils):
diff --git a/hooks/charmhelpers/contrib/openstack/cert_utils.py b/hooks/charmhelpers/contrib/openstack/cert_utils.py
index 3e07870..3a3c6de 100644
--- a/hooks/charmhelpers/contrib/openstack/cert_utils.py
+++ b/hooks/charmhelpers/contrib/openstack/cert_utils.py
@@ -195,7 +195,7 @@ def install_certs(ssl_dir, certs, chain=None):
if chain:
# Append chain file so that clients that trust the root CA will
# trust certs signed by an intermediate in the chain
- cert_data = cert_data + chain
+ cert_data = cert_data + os.linesep + chain
write_file(
path=os.path.join(ssl_dir, cert_filename),
content=cert_data, perms=0o640)
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index 72084cb..614d444 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -98,7 +98,6 @@ from charmhelpers.contrib.network.ip import (
from charmhelpers.contrib.openstack.utils import (
config_flags_parser,
enable_memcache,
- snap_install_requested,
CompareOpenStackReleases,
os_release,
)
@@ -252,13 +251,8 @@ class SharedDBContext(OSContextGenerator):
'database': self.database,
'database_user': self.user,
'database_password': rdata.get(password_setting),
- 'database_type': 'mysql'
+ 'database_type': 'mysql+pymysql'
}
- # Note(coreycb): We can drop mysql+pymysql if we want when the
- # following review lands, though it seems mysql+pymysql would
- # be preferred. https://review.openstack.org/#/c/462190/
- if snap_install_requested():
- ctxt['database_type'] = 'mysql+pymysql'
if self.context_complete(ctxt):
db_ssl(rdata, ctxt, self.ssl_dir)
return ctxt
diff --git a/hooks/charmhelpers/contrib/openstack/ha/utils.py b/hooks/charmhelpers/contrib/openstack/ha/utils.py
index add8eb9..718c6d6 100644
--- a/hooks/charmhelpers/contrib/openstack/ha/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/ha/utils.py
@@ -23,6 +23,7 @@
Helpers for high availability.
"""
+import hashlib
import json
import re
@@ -35,7 +36,6 @@ from charmhelpers.core.hookenv import (
config,
status_set,
DEBUG,
- WARNING,
)
from charmhelpers.core.host import (
@@ -63,6 +63,9 @@ JSON_ENCODE_OPTIONS = dict(
separators=(',', ':'),
)
+VIP_GROUP_NAME = 'grp_{service}_vips'
+DNSHA_GROUP_NAME = 'grp_{service}_hostnames'
+
class DNSHAException(Exception):
"""Raised when an error occurs setting up DNS HA
@@ -124,13 +127,29 @@ def expect_ha():
return len(ha_related_units) > 0 or config('vip') or config('dns-ha')
-def generate_ha_relation_data(service):
+def generate_ha_relation_data(service, extra_settings=None):
""" Generate relation data for ha relation
Based on configuration options and unit interfaces, generate a json
encoded dict of relation data items for the hacluster relation,
providing configuration for DNS HA or VIP's + haproxy clone sets.
+ Example of supplying additional settings::
+
+ COLO_CONSOLEAUTH = 'inf: res_nova_consoleauth grp_nova_vips'
+ AGENT_CONSOLEAUTH = 'ocf:openstack:nova-consoleauth'
+ AGENT_CA_PARAMS = 'op monitor interval="5s"'
+
+ ha_console_settings = {
+ 'colocations': {'vip_consoleauth': COLO_CONSOLEAUTH},
+ 'init_services': {'res_nova_consoleauth': 'nova-consoleauth'},
+ 'resources': {'res_nova_consoleauth': AGENT_CONSOLEAUTH},
+ 'resource_params': {'res_nova_consoleauth': AGENT_CA_PARAMS})
+ generate_ha_relation_data('nova', extra_settings=ha_console_settings)
+
+
+ @param service: Name of the service being configured
+ @param extra_settings: Dict of additional resource data
@returns dict: json encoded data for use with relation_set
"""
_haproxy_res = 'res_{}_haproxy'.format(service)
@@ -149,6 +168,13 @@ def generate_ha_relation_data(service):
},
}
+ if extra_settings:
+ for k, v in extra_settings.items():
+ if _relation_data.get(k):
+ _relation_data[k].update(v)
+ else:
+ _relation_data[k] = v
+
if config('dns-ha'):
update_hacluster_dns_ha(service, _relation_data)
else:
@@ -216,7 +242,7 @@ def update_hacluster_dns_ha(service, relation_data,
'Informing the ha relation'.format(' '.join(hostname_group)),
DEBUG)
relation_data['groups'] = {
- 'grp_{}_hostnames'.format(service): ' '.join(hostname_group)
+ DNSHA_GROUP_NAME.format(service=service): ' '.join(hostname_group)
}
else:
msg = 'DNS HA: Hostname group has no members.'
@@ -224,6 +250,27 @@ def update_hacluster_dns_ha(service, relation_data,
raise DNSHAException(msg)
+def get_vip_settings(vip):
+ """Calculate which nic is on the correct network for the given vip.
+
+ If nic or netmask discovery fail then fallback to using charm supplied
+ config. If fallback is used this is indicated via the fallback variable.
+
+ @param vip: VIP to lookup nic and cidr for.
+ @returns (str, str, bool): eg (iface, netmask, fallback)
+ """
+ iface = get_iface_for_address(vip)
+ netmask = get_netmask_for_address(vip)
+ fallback = False
+ if iface is None:
+ iface = config('vip_iface')
+ fallback = True
+ if netmask is None:
+ netmask = config('vip_cidr')
+ fallback = True
+ return iface, netmask, fallback
+
+
def update_hacluster_vip(service, relation_data):
""" Configure VIP resources based on provided configuration
@@ -232,40 +279,70 @@ def update_hacluster_vip(service, relation_data):
"""
cluster_config = get_hacluster_config()
vip_group = []
+ vips_to_delete = []
for vip in cluster_config['vip'].split():
if is_ipv6(vip):
- res_neutron_vip = 'ocf:heartbeat:IPv6addr'
+ res_vip = 'ocf:heartbeat:IPv6addr'
vip_params = 'ipv6addr'
else:
- res_neutron_vip = 'ocf:heartbeat:IPaddr2'
+ res_vip = 'ocf:heartbeat:IPaddr2'
vip_params = 'ip'
- iface = (get_iface_for_address(vip) or
- config('vip_iface'))
- netmask = (get_netmask_for_address(vip) or
- config('vip_cidr'))
+ iface, netmask, fallback = get_vip_settings(vip)
+ vip_monitoring = 'op monitor depth="0" timeout="20s" interval="10s"'
if iface is not None:
+ # NOTE(jamespage): Delete old VIP resources
+ # Old style naming encoding iface in name
+ # does not work well in environments where
+ # interface/subnet wiring is not consistent
vip_key = 'res_{}_{}_vip'.format(service, iface)
- if vip_key in vip_group:
- if vip not in relation_data['resource_params'][vip_key]:
- vip_key = '{}_{}'.format(vip_key, vip_params)
- else:
- log("Resource '%s' (vip='%s') already exists in "
- "vip group - skipping" % (vip_key, vip), WARNING)
- continue
-
- relation_data['resources'][vip_key] = res_neutron_vip
- relation_data['resource_params'][vip_key] = (
- 'params {ip}="{vip}" cidr_netmask="{netmask}" '
- 'nic="{iface}"'.format(ip=vip_params,
- vip=vip,
- iface=iface,
- netmask=netmask)
- )
+ if vip_key in vips_to_delete:
+ vip_key = '{}_{}'.format(vip_key, vip_params)
+ vips_to_delete.append(vip_key)
+
+ vip_key = 'res_{}_{}_vip'.format(
+ service,
+ hashlib.sha1(vip.encode('UTF-8')).hexdigest()[:7])
+
+ relation_data['resources'][vip_key] = res_vip
+ # NOTE(jamespage):
+ # Use option provided vip params if these where used
+ # instead of auto-detected values
+ if fallback:
+ relation_data['resource_params'][vip_key] = (
+ 'params {ip}="{vip}" cidr_netmask="{netmask}" '
+ 'nic="{iface}" {vip_monitoring}'.format(
+ ip=vip_params,
+ vip=vip,
+ iface=iface,
+ netmask=netmask,
+ vip_monitoring=vip_monitoring))
+ else:
+ # NOTE(jamespage):
+ # let heartbeat figure out which interface and
+ # netmask to configure, which works nicely
+ # when network interface naming is not
+ # consistent across units.
+ relation_data['resource_params'][vip_key] = (
+ 'params {ip}="{vip}" {vip_monitoring}'.format(
+ ip=vip_params,
+ vip=vip,
+ vip_monitoring=vip_monitoring))
+
vip_group.append(vip_key)
+ if vips_to_delete:
+ try:
+ relation_data['delete_resources'].extend(vips_to_delete)
+ except KeyError:
+ relation_data['delete_resources'] = vips_to_delete
+
if len(vip_group) >= 1:
- relation_data['groups'] = {
- 'grp_{}_vips'.format(service): ' '.join(vip_group)
- }
+ key = VIP_GROUP_NAME.format(service=service)
+ try:
+ relation_data['groups'][key] = ' '.join(vip_group)
+ except KeyError:
+ relation_data['groups'] = {
+ key: ' '.join(vip_group)
+ }
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index 29cad08..4e432a2 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -73,6 +73,8 @@ from charmhelpers.core.host import (
service_running,
service_pause,
service_resume,
+ service_stop,
+ service_start,
restart_on_change_helper,
)
from charmhelpers.fetch import (
@@ -116,6 +118,7 @@ OPENSTACK_RELEASES = (
'pike',
'queens',
'rocky',
+ 'stein',
)
UBUNTU_OPENSTACK_RELEASE = OrderedDict([
@@ -134,6 +137,7 @@ UBUNTU_OPENSTACK_RELEASE = OrderedDict([
('artful', 'pike'),
('bionic', 'queens'),
('cosmic', 'rocky'),
+ ('disco', 'stein'),
])
@@ -153,6 +157,7 @@ OPENSTACK_CODENAMES = OrderedDict([
('2017.2', 'pike'),
('2018.1', 'queens'),
('2018.2', 'rocky'),
+ ('2019.1', 'stein'),
])
# The ugly duckling - must list releases oldest to newest
@@ -187,6 +192,8 @@ SWIFT_CODENAMES = OrderedDict([
['2.16.0', '2.17.0']),
('rocky',
['2.18.0', '2.19.0']),
+ ('stein',
+ ['2.19.0']),
])
# >= Liberty version->codename mapping
@@ -199,6 +206,7 @@ PACKAGE_CODENAMES = {
('16', 'pike'),
('17', 'queens'),
('18', 'rocky'),
+ ('19', 'stein'),
]),
'neutron-common': OrderedDict([
('7', 'liberty'),
@@ -208,6 +216,7 @@ PACKAGE_CODENAMES = {
('11', 'pike'),
('12', 'queens'),
('13', 'rocky'),
+ ('14', 'stein'),
]),
'cinder-common': OrderedDict([
('7', 'liberty'),
@@ -217,6 +226,7 @@ PACKAGE_CODENAMES = {
('11', 'pike'),
('12', 'queens'),
('13', 'rocky'),
+ ('14', 'stein'),
]),
'keystone': OrderedDict([
('8', 'liberty'),
@@ -226,6 +236,7 @@ PACKAGE_CODENAMES = {
('12', 'pike'),
('13', 'queens'),
('14', 'rocky'),
+ ('15', 'stein'),
]),
'horizon-common': OrderedDict([
('8', 'liberty'),
@@ -235,6 +246,7 @@ PACKAGE_CODENAMES = {
('12', 'pike'),
('13', 'queens'),
('14', 'rocky'),
+ ('15', 'stein'),
]),
'ceilometer-common': OrderedDict([
('5', 'liberty'),
@@ -244,6 +256,7 @@ PACKAGE_CODENAMES = {
('9', 'pike'),
('10', 'queens'),
('11', 'rocky'),
+ ('12', 'stein'),
]),
'heat-common': OrderedDict([
('5', 'liberty'),
@@ -253,6 +266,7 @@ PACKAGE_CODENAMES = {
('9', 'pike'),
('10', 'queens'),
('11', 'rocky'),
+ ('12', 'stein'),
]),
'glance-common': OrderedDict([
('11', 'liberty'),
@@ -262,6 +276,7 @@ PACKAGE_CODENAMES = {
('15', 'pike'),
('16', 'queens'),
('17', 'rocky'),
+ ('18', 'stein'),
]),
'openstack-dashboard': OrderedDict([
('8', 'liberty'),
@@ -271,6 +286,7 @@ PACKAGE_CODENAMES = {
('12', 'pike'),
('13', 'queens'),
('14', 'rocky'),
+ ('15', 'stein'),
]),
}
@@ -299,7 +315,7 @@ def get_os_codename_install_source(src):
rel = ''
if src is None:
return rel
- if src in ['distro', 'distro-proposed']:
+ if src in ['distro', 'distro-proposed', 'proposed']:
try:
rel = UBUNTU_OPENSTACK_RELEASE[ubuntu_rel]
except KeyError:
@@ -1303,6 +1319,65 @@ def is_unit_paused_set():
return False
+def manage_payload_services(action, services=None, charm_func=None):
+ """Run an action against all services.
+
+ An optional charm_func() can be called. It should raise an Exception to
+ indicate that the function failed. If it was succesfull it should return
+ None or an optional message.
+
+ The signature for charm_func is:
+ charm_func() -> message: str
+
+ charm_func() is executed after any services are stopped, if supplied.
+
+ The services object can either be:
+ - None : no services were passed (an empty dict is returned)
+ - a list of strings
+ - A dictionary (optionally OrderedDict) {service_name: {'service': ..}}
+ - An array of [{'service': service_name, ...}, ...]
+
+ :param action: Action to run: pause, resume, start or stop.
+ :type action: str
+ :param services: See above
+ :type services: See above
+ :param charm_func: function to run for custom charm pausing.
+ :type charm_func: f()
+ :returns: Status boolean and list of messages
+ :rtype: (bool, [])
+ :raises: RuntimeError
+ """
+ actions = {
+ 'pause': service_pause,
+ 'resume': service_resume,
+ 'start': service_start,
+ 'stop': service_stop}
+ action = action.lower()
+ if action not in actions.keys():
+ raise RuntimeError(
+ "action: {} must be one of: {}".format(action,
+ ', '.join(actions.keys())))
+ services = _extract_services_list_helper(services)
+ messages = []
+ success = True
+ if services:
+ for service in services.keys():
+ rc = actions[action](service)
+ if not rc:
+ success = False
+ messages.append("{} didn't {} cleanly.".format(service,
+ action))
+ if charm_func:
+ try:
+ message = charm_func()
+ if message:
+ messages.append(message)
+ except Exception as e:
+ success = False
+ messages.append(str(e))
+ return success, messages
+
+
def pause_unit(assess_status_func, services=None, ports=None,
charm_func=None):
"""Pause a unit by stopping the services and setting 'unit-paused'
@@ -1333,20 +1408,10 @@ def pause_unit(assess_status_func, services=None, ports=None,
@returns None
@raises Exception(message) on an error for action_fail().
"""
- services = _extract_services_list_helper(services)
- messages = []
- if services:
- for service in services.keys():
- stopped = service_pause(service)
- if not stopped:
- messages.append("{} didn't stop cleanly.".format(service))
- if charm_func:
- try:
- message = charm_func()
- if message:
- messages.append(message)
- except Exception as e:
- message.append(str(e))
+ _, messages = manage_payload_services(
+ 'pause',
+ services=services,
+ charm_func=charm_func)
set_unit_paused()
if assess_status_func:
message = assess_status_func()
@@ -1385,20 +1450,10 @@ def resume_unit(assess_status_func, services=None, ports=None,
@returns None
@raises Exception(message) on an error for action_fail().
"""
- services = _extract_services_list_helper(services)
- messages = []
- if services:
- for service in services.keys():
- started = service_resume(service)
- if not started:
- messages.append("{} didn't start cleanly.".format(service))
- if charm_func:
- try:
- message = charm_func()
- if message:
- messages.append(message)
- except Exception as e:
- message.append(str(e))
+ _, messages = manage_payload_services(
+ 'resume',
+ services=services,
+ charm_func=charm_func)
clear_unit_paused()
if assess_status_func:
message = assess_status_func()
diff --git a/hooks/charmhelpers/contrib/storage/linux/loopback.py b/hooks/charmhelpers/contrib/storage/linux/loopback.py
index 0dfdae5..82472ff 100644
--- a/hooks/charmhelpers/contrib/storage/linux/loopback.py
+++ b/hooks/charmhelpers/contrib/storage/linux/loopback.py
@@ -36,8 +36,10 @@ def loopback_devices():
'''
loopbacks = {}
cmd = ['losetup', '-a']
- devs = [d.strip().split(' ') for d in
- check_output(cmd).splitlines() if d != '']
+ output = check_output(cmd)
+ if six.PY3:
+ output = output.decode('utf-8')
+ devs = [d.strip().split(' ') for d in output.splitlines() if d != '']
for dev, _, f in devs:
loopbacks[dev.replace(':', '')] = re.search(r'\((\S+)\)', f).groups()[0]
return loopbacks
diff --git a/hooks/charmhelpers/fetch/ubuntu.py b/hooks/charmhelpers/fetch/ubuntu.py
index c7ad128..8a5cadf 100644
--- a/hooks/charmhelpers/fetch/ubuntu.py
+++ b/hooks/charmhelpers/fetch/ubuntu.py
@@ -166,6 +166,14 @@ CLOUD_ARCHIVE_POCKETS = {
'rocky/proposed': 'bionic-proposed/rocky',
'bionic-rocky/proposed': 'bionic-proposed/rocky',
'bionic-proposed/rocky': 'bionic-proposed/rocky',
+ # Stein
+ 'stein': 'bionic-updates/stein',
+ 'bionic-stein': 'bionic-updates/stein',
+ 'bionic-stein/updates': 'bionic-updates/stein',
+ 'bionic-updates/stein': 'bionic-updates/stein',
+ 'stein/proposed': 'bionic-proposed/stein',
+ 'bionic-stein/proposed': 'bionic-proposed/stein',
+ 'bionic-proposed/stein': 'bionic-proposed/stein',
}

This mirror site include all the OpenStack related repositories under: openstack, openstack-dev and openstack-infra.

NOTE: All repositories are updated every one hour.

Usage

For Git Clone
 git clone http://git.trystack.cn/openstack/nova.git 
For DevStack

Add GIT_BASE, NOVNC_REPO and SPICE_REPO variables to local.conf file.

[[local|localrc]]

# use TryStack git mirror
GIT_BASE=http://git.trystack.cn
NOVNC_REPO=http://git.trystack.cn/kanaka/noVNC.git
SPICE_REPO=http://git.trystack.cn/git/spice/spice-html5.git