summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorChris MacNaughton <chris.macnaughton@canonical.com>2019-04-05 17:14:50 +0200
committerChris MacNaughton <chris.macnaughton@canonical.com>2019-04-05 17:14:59 +0200
commit47cd8e8f28f0d8689411595356234118eebeab1f (patch)
tree104438ccce258eff63a165b83e922ac3fc28d619
parentade8687ad831c0922dc707d198b3bc9b8a34c374 (diff)
downloadcharm-nova-lxd-47cd8e8f28f0d8689411595356234118eebeab1f.zip
charm-nova-lxd-47cd8e8f28f0d8689411595356234118eebeab1f.tar.gz
charm-nova-lxd-47cd8e8f28f0d8689411595356234118eebeab1f.tar.bz2
Sync charm-helpers to enable Ubuntu Disco
Change-Id: Ifebdc397e14aa67b0e5d00e1c86592b4e14b6088
-rw-r--r--hooks/charmhelpers/contrib/openstack/amulet/deployment.py1
-rw-r--r--hooks/charmhelpers/contrib/openstack/audits/__init__.py212
-rw-r--r--hooks/charmhelpers/contrib/openstack/audits/openstack_security_guide.py266
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/logrotate9
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/section-oslo-messaging-rabbit10
-rw-r--r--hooks/charmhelpers/contrib/python.py21
-rw-r--r--hooks/charmhelpers/core/host_factory/ubuntu.py1
-rw-r--r--hooks/charmhelpers/fetch/python/__init__.py13
-rw-r--r--hooks/charmhelpers/fetch/python/debug.py54
-rw-r--r--hooks/charmhelpers/fetch/python/packages.py154
-rw-r--r--hooks/charmhelpers/fetch/python/rpdb.py56
-rw-r--r--hooks/charmhelpers/fetch/python/version.py32
12 files changed, 829 insertions, 0 deletions
diff --git a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
index d1270a7..8e57467 100644
--- a/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
+++ b/hooks/charmhelpers/contrib/openstack/amulet/deployment.py
@@ -312,6 +312,7 @@ class OpenStackAmuletDeployment(AmuletDeployment):
('artful', 'pike'),
('bionic', 'queens'),
('cosmic', 'rocky'),
+ ('disco', 'stein'),
])
if self.openstack:
os_origin = self.openstack.split(':')[1]
diff --git a/hooks/charmhelpers/contrib/openstack/audits/__init__.py b/hooks/charmhelpers/contrib/openstack/audits/__init__.py
new file mode 100644
index 0000000..7f7e5f7
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/audits/__init__.py
@@ -0,0 +1,212 @@
+# Copyright 2019 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""OpenStack Security Audit code"""
+
+import collections
+from enum import Enum
+import traceback
+
+from charmhelpers.core.host import cmp_pkgrevno
+import charmhelpers.contrib.openstack.utils as openstack_utils
+import charmhelpers.core.hookenv as hookenv
+
+
+class AuditType(Enum):
+ OpenStackSecurityGuide = 1
+
+
+_audits = {}
+
+Audit = collections.namedtuple('Audit', 'func filters')
+
+
+def audit(*args):
+ """Decorator to register an audit.
+
+ These are used to generate audits that can be run on a
+ deployed system that matches the given configuration
+
+ :param args: List of functions to filter tests against
+ :type args: List[Callable[Dict]]
+ """
+ def wrapper(f):
+ test_name = f.__name__
+ if _audits.get(test_name):
+ raise RuntimeError(
+ "Test name '{}' used more than once"
+ .format(test_name))
+ non_callables = [fn for fn in args if not callable(fn)]
+ if non_callables:
+ raise RuntimeError(
+ "Configuration includes non-callable filters: {}"
+ .format(non_callables))
+ _audits[test_name] = Audit(func=f, filters=args)
+ return f
+ return wrapper
+
+
+def is_audit_type(*args):
+ """This audit is included in the specified kinds of audits.
+
+ :param *args: List of AuditTypes to include this audit in
+ :type args: List[AuditType]
+ :rtype: Callable[Dict]
+ """
+ def _is_audit_type(audit_options):
+ if audit_options.get('audit_type') in args:
+ return True
+ else:
+ return False
+ return _is_audit_type
+
+
+def since_package(pkg, pkg_version):
+ """This audit should be run after the specified package version (incl).
+
+ :param pkg: Package name to compare
+ :type pkg: str
+ :param release: The package version
+ :type release: str
+ :rtype: Callable[Dict]
+ """
+ def _since_package(audit_options=None):
+ return cmp_pkgrevno(pkg, pkg_version) >= 0
+
+ return _since_package
+
+
+def before_package(pkg, pkg_version):
+ """This audit should be run before the specified package version (excl).
+
+ :param pkg: Package name to compare
+ :type pkg: str
+ :param release: The package version
+ :type release: str
+ :rtype: Callable[Dict]
+ """
+ def _before_package(audit_options=None):
+ return not since_package(pkg, pkg_version)()
+
+ return _before_package
+
+
+def since_openstack_release(pkg, release):
+ """This audit should run after the specified OpenStack version (incl).
+
+ :param pkg: Package name to compare
+ :type pkg: str
+ :param release: The OpenStack release codename
+ :type release: str
+ :rtype: Callable[Dict]
+ """
+ def _since_openstack_release(audit_options=None):
+ _release = openstack_utils.get_os_codename_package(pkg)
+ return openstack_utils.CompareOpenStackReleases(_release) >= release
+
+ return _since_openstack_release
+
+
+def before_openstack_release(pkg, release):
+ """This audit should run before the specified OpenStack version (excl).
+
+ :param pkg: Package name to compare
+ :type pkg: str
+ :param release: The OpenStack release codename
+ :type release: str
+ :rtype: Callable[Dict]
+ """
+ def _before_openstack_release(audit_options=None):
+ return not since_openstack_release(pkg, release)()
+
+ return _before_openstack_release
+
+
+def it_has_config(config_key):
+ """This audit should be run based on specified config keys.
+
+ :param config_key: Config key to look for
+ :type config_key: str
+ :rtype: Callable[Dict]
+ """
+ def _it_has_config(audit_options):
+ return audit_options.get(config_key) is not None
+
+ return _it_has_config
+
+
+def run(audit_options):
+ """Run the configured audits with the specified audit_options.
+
+ :param audit_options: Configuration for the audit
+ :type audit_options: Config
+
+ :rtype: Dict[str, str]
+ """
+ errors = {}
+ results = {}
+ for name, audit in sorted(_audits.items()):
+ result_name = name.replace('_', '-')
+ if result_name in audit_options.get('excludes', []):
+ print(
+ "Skipping {} because it is"
+ "excluded in audit config"
+ .format(result_name))
+ continue
+ if all(p(audit_options) for p in audit.filters):
+ try:
+ audit.func(audit_options)
+ print("{}: PASS".format(name))
+ results[result_name] = {
+ 'success': True,
+ }
+ except AssertionError as e:
+ print("{}: FAIL ({})".format(name, e))
+ results[result_name] = {
+ 'success': False,
+ 'message': e,
+ }
+ except Exception as e:
+ print("{}: ERROR ({})".format(name, e))
+ errors[name] = e
+ results[result_name] = {
+ 'success': False,
+ 'message': e,
+ }
+ for name, error in errors.items():
+ print("=" * 20)
+ print("Error in {}: ".format(name))
+ traceback.print_tb(error.__traceback__)
+ print()
+ return results
+
+
+def action_parse_results(result):
+ """Parse the result of `run` in the context of an action.
+
+ :param result: The result of running the security-checklist
+ action on a unit
+ :type result: Dict[str, Dict[str, str]]
+ :rtype: int
+ """
+ passed = True
+ for test, result in result.items():
+ if result['success']:
+ hookenv.action_set({test: 'PASS'})
+ else:
+ hookenv.action_set({test: 'FAIL - {}'.format(result['message'])})
+ passed = False
+ if not passed:
+ hookenv.action_fail("One or more tests failed")
+ return 0 if passed else 1
diff --git a/hooks/charmhelpers/contrib/openstack/audits/openstack_security_guide.py b/hooks/charmhelpers/contrib/openstack/audits/openstack_security_guide.py
new file mode 100644
index 0000000..e5b7ac1
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/audits/openstack_security_guide.py
@@ -0,0 +1,266 @@
+# Copyright 2019 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import collections
+import configparser
+import glob
+import os.path
+import subprocess
+
+from charmhelpers.contrib.openstack.audits import (
+ audit,
+ AuditType,
+ # filters
+ is_audit_type,
+ it_has_config,
+)
+
+from charmhelpers.core.hookenv import (
+ cached,
+)
+
+"""
+The Security Guide suggests a specific list of files inside the
+config directory for the service having 640 specifically, but
+by ensuring the containing directory is 750, only the owner can
+write, and only the group can read files within the directory.
+
+By restricting access to the containing directory, we can more
+effectively ensure that there is no accidental leakage if a new
+file is added to the service without being added to the security
+guide, and to this check.
+"""
+FILE_ASSERTIONS = {
+ 'barbican': {
+ '/etc/barbican': {'group': 'barbican', 'mode': '750'},
+ },
+ 'ceph-mon': {
+ '/var/lib/charm/ceph-mon/ceph.conf':
+ {'owner': 'root', 'group': 'root', 'mode': '644'},
+ '/etc/ceph/ceph.client.admin.keyring':
+ {'owner': 'ceph', 'group': 'ceph'},
+ '/etc/ceph/rbdmap': {'mode': '644'},
+ '/var/lib/ceph': {'owner': 'ceph', 'group': 'ceph', 'mode': '750'},
+ '/var/lib/ceph/bootstrap-*/ceph.keyring':
+ {'owner': 'ceph', 'group': 'ceph', 'mode': '600'}
+ },
+ 'ceph-osd': {
+ '/var/lib/charm/ceph-osd/ceph.conf':
+ {'owner': 'ceph', 'group': 'ceph', 'mode': '644'},
+ '/var/lib/ceph': {'owner': 'ceph', 'group': 'ceph', 'mode': '750'},
+ '/var/lib/ceph/*': {'owner': 'ceph', 'group': 'ceph', 'mode': '755'},
+ '/var/lib/ceph/bootstrap-*/ceph.keyring':
+ {'owner': 'ceph', 'group': 'ceph', 'mode': '600'},
+ '/var/lib/ceph/radosgw':
+ {'owner': 'ceph', 'group': 'ceph', 'mode': '755'},
+ },
+ 'cinder': {
+ '/etc/cinder': {'group': 'cinder', 'mode': '750'},
+ },
+ 'glance': {
+ '/etc/glance': {'group': 'glance', 'mode': '750'},
+ },
+ 'keystone': {
+ '/etc/keystone':
+ {'owner': 'keystone', 'group': 'keystone', 'mode': '750'},
+ },
+ 'manilla': {
+ '/etc/manila': {'group': 'manilla', 'mode': '750'},
+ },
+ 'neutron-gateway': {
+ '/etc/neutron': {'group': 'neutron', 'mode': '750'},
+ },
+ 'neutron-api': {
+ '/etc/neutron/': {'group': 'neutron', 'mode': '750'},
+ },
+ 'nova-cloud-controller': {
+ '/etc/nova': {'group': 'nova', 'mode': '750'},
+ },
+ 'nova-compute': {
+ '/etc/nova/': {'group': 'nova', 'mode': '750'},
+ },
+ 'openstack-dashboard': {
+ # From security guide
+ '/etc/openstack-dashboard/local_settings.py':
+ {'group': 'horizon', 'mode': '640'},
+ },
+}
+
+Ownership = collections.namedtuple('Ownership', 'owner group mode')
+
+
+@cached
+def _stat(file):
+ """
+ Get the Ownership information from a file.
+
+ :param file: The path to a file to stat
+ :type file: str
+ :returns: owner, group, and mode of the specified file
+ :rtype: Ownership
+ :raises subprocess.CalledProcessError: If the underlying stat fails
+ """
+ out = subprocess.check_output(
+ ['stat', '-c', '%U %G %a', file]).decode('utf-8')
+ return Ownership(*out.strip().split(' '))
+
+
+@cached
+def _config_ini(path):
+ """
+ Parse an ini file
+
+ :param path: The path to a file to parse
+ :type file: str
+ :returns: Configuration contained in path
+ :rtype: Dict
+ """
+ conf = configparser.ConfigParser()
+ conf.read(path)
+ return dict(conf)
+
+
+def _validate_file_ownership(owner, group, file_name, optional=False):
+ """
+ Validate that a specified file is owned by `owner:group`.
+
+ :param owner: Name of the owner
+ :type owner: str
+ :param group: Name of the group
+ :type group: str
+ :param file_name: Path to the file to verify
+ :type file_name: str
+ :param optional: Is this file optional,
+ ie: Should this test fail when it's missing
+ :type optional: bool
+ """
+ try:
+ ownership = _stat(file_name)
+ except subprocess.CalledProcessError as e:
+ print("Error reading file: {}".format(e))
+ if not optional:
+ assert False, "Specified file does not exist: {}".format(file_name)
+ assert owner == ownership.owner, \
+ "{} has an incorrect owner: {} should be {}".format(
+ file_name, ownership.owner, owner)
+ assert group == ownership.group, \
+ "{} has an incorrect group: {} should be {}".format(
+ file_name, ownership.group, group)
+ print("Validate ownership of {}: PASS".format(file_name))
+
+
+def _validate_file_mode(mode, file_name, optional=False):
+ """
+ Validate that a specified file has the specified permissions.
+
+ :param mode: file mode that is desires
+ :type owner: str
+ :param file_name: Path to the file to verify
+ :type file_name: str
+ :param optional: Is this file optional,
+ ie: Should this test fail when it's missing
+ :type optional: bool
+ """
+ try:
+ ownership = _stat(file_name)
+ except subprocess.CalledProcessError as e:
+ print("Error reading file: {}".format(e))
+ if not optional:
+ assert False, "Specified file does not exist: {}".format(file_name)
+ assert mode == ownership.mode, \
+ "{} has an incorrect mode: {} should be {}".format(
+ file_name, ownership.mode, mode)
+ print("Validate mode of {}: PASS".format(file_name))
+
+
+@cached
+def _config_section(config, section):
+ """Read the configuration file and return a section."""
+ path = os.path.join(config.get('config_path'), config.get('config_file'))
+ conf = _config_ini(path)
+ return conf.get(section)
+
+
+@audit(is_audit_type(AuditType.OpenStackSecurityGuide),
+ it_has_config('files'))
+def validate_file_ownership(config):
+ """Verify that configuration files are owned by the correct user/group."""
+ files = config.get('files', {})
+ for file_name, options in files.items():
+ for key in options.keys():
+ if key not in ["owner", "group", "mode"]:
+ raise RuntimeError(
+ "Invalid ownership configuration: {}".format(key))
+ owner = options.get('owner', config.get('owner', 'root'))
+ group = options.get('group', config.get('group', 'root'))
+ optional = options.get('optional', config.get('optional', 'False'))
+ if '*' in file_name:
+ for file in glob.glob(file_name):
+ if file not in files.keys():
+ if os.path.isfile(file):
+ _validate_file_ownership(owner, group, file, optional)
+ else:
+ if os.path.isfile(file_name):
+ _validate_file_ownership(owner, group, file_name, optional)
+
+
+@audit(is_audit_type(AuditType.OpenStackSecurityGuide),
+ it_has_config('files'))
+def validate_file_permissions(config):
+ """Verify that permissions on configuration files are secure enough."""
+ files = config.get('files', {})
+ for file_name, options in files.items():
+ for key in options.keys():
+ if key not in ["owner", "group", "mode"]:
+ raise RuntimeError(
+ "Invalid ownership configuration: {}".format(key))
+ mode = options.get('mode', config.get('permissions', '600'))
+ optional = options.get('optional', config.get('optional', 'False'))
+ if '*' in file_name:
+ for file in glob.glob(file_name):
+ if file not in files.keys():
+ if os.path.isfile(file):
+ _validate_file_mode(mode, file, optional)
+ else:
+ if os.path.isfile(file_name):
+ _validate_file_mode(mode, file_name, optional)
+
+
+@audit(is_audit_type(AuditType.OpenStackSecurityGuide))
+def validate_uses_keystone(audit_options):
+ """Validate that the service uses Keystone for authentication."""
+ section = _config_section(audit_options, 'DEFAULT')
+ assert section is not None, "Missing section 'DEFAULT'"
+ assert section.get('auth_strategy') == "keystone", \
+ "Application is not using Keystone"
+
+
+@audit(is_audit_type(AuditType.OpenStackSecurityGuide))
+def validate_uses_tls_for_keystone(audit_options):
+ """Verify that TLS is used to communicate with Keystone."""
+ section = _config_section(audit_options, 'keystone_authtoken')
+ assert section is not None, "Missing section 'keystone_authtoken'"
+ assert not section.get('insecure') and \
+ "https://" in section.get("auth_uri"), \
+ "TLS is not used for Keystone"
+
+
+@audit(is_audit_type(AuditType.OpenStackSecurityGuide))
+def validate_uses_tls_for_glance(audit_options):
+ """Verify that TLS is used to communicate with Glance."""
+ section = _config_section(audit_options, 'glance')
+ assert section is not None, "Missing section 'glance'"
+ assert not section.get('insecure') and \
+ "https://" in section.get("api_servers"), \
+ "TLS is not used for Glance"
diff --git a/hooks/charmhelpers/contrib/openstack/templates/logrotate b/hooks/charmhelpers/contrib/openstack/templates/logrotate
new file mode 100644
index 0000000..b2900d0
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/templates/logrotate
@@ -0,0 +1,9 @@
+/var/log/{{ logrotate_logs_location }}/*.log {
+ {{ logrotate_interval }}
+ {{ logrotate_count }}
+ compress
+ delaycompress
+ missingok
+ notifempty
+ copytruncate
+}
diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-oslo-messaging-rabbit b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-messaging-rabbit
new file mode 100644
index 0000000..bed2216
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-messaging-rabbit
@@ -0,0 +1,10 @@
+[oslo_messaging_rabbit]
+{% if rabbitmq_ha_queues -%}
+rabbit_ha_queues = True
+{% endif -%}
+{% if rabbit_ssl_port -%}
+ssl = True
+{% endif -%}
+{% if rabbit_ssl_ca -%}
+ssl_ca_file = {{ rabbit_ssl_ca }}
+{% endif -%}
diff --git a/hooks/charmhelpers/contrib/python.py b/hooks/charmhelpers/contrib/python.py
new file mode 100644
index 0000000..84cba8c
--- /dev/null
+++ b/hooks/charmhelpers/contrib/python.py
@@ -0,0 +1,21 @@
+# Copyright 2014-2019 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import absolute_import
+
+# deprecated aliases for backwards compatibility
+from charmhelpers.fetch.python import debug # noqa
+from charmhelpers.fetch.python import packages # noqa
+from charmhelpers.fetch.python import rpdb # noqa
+from charmhelpers.fetch.python import version # noqa
diff --git a/hooks/charmhelpers/core/host_factory/ubuntu.py b/hooks/charmhelpers/core/host_factory/ubuntu.py
index a3162fa..0ee2b66 100644
--- a/hooks/charmhelpers/core/host_factory/ubuntu.py
+++ b/hooks/charmhelpers/core/host_factory/ubuntu.py
@@ -23,6 +23,7 @@ UBUNTU_RELEASES = (
'artful',
'bionic',
'cosmic',
+ 'disco',
)
diff --git a/hooks/charmhelpers/fetch/python/__init__.py b/hooks/charmhelpers/fetch/python/__init__.py
new file mode 100644
index 0000000..bff99dc
--- /dev/null
+++ b/hooks/charmhelpers/fetch/python/__init__.py
@@ -0,0 +1,13 @@
+# Copyright 2014-2019 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
diff --git a/hooks/charmhelpers/fetch/python/debug.py b/hooks/charmhelpers/fetch/python/debug.py
new file mode 100644
index 0000000..757135e
--- /dev/null
+++ b/hooks/charmhelpers/fetch/python/debug.py
@@ -0,0 +1,54 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright 2014-2015 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+from __future__ import print_function
+
+import atexit
+import sys
+
+from charmhelpers.fetch.python.rpdb import Rpdb
+from charmhelpers.core.hookenv import (
+ open_port,
+ close_port,
+ ERROR,
+ log
+)
+
+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
+
+DEFAULT_ADDR = "0.0.0.0"
+DEFAULT_PORT = 4444
+
+
+def _error(message):
+ log(message, level=ERROR)
+
+
+def set_trace(addr=DEFAULT_ADDR, port=DEFAULT_PORT):
+ """
+ Set a trace point using the remote debugger
+ """
+ atexit.register(close_port, port)
+ try:
+ log("Starting a remote python debugger session on %s:%s" % (addr,
+ port))
+ open_port(port)
+ debugger = Rpdb(addr=addr, port=port)
+ debugger.set_trace(sys._getframe().f_back)
+ except Exception:
+ _error("Cannot start a remote debug session on %s:%s" % (addr,
+ port))
diff --git a/hooks/charmhelpers/fetch/python/packages.py b/hooks/charmhelpers/fetch/python/packages.py
new file mode 100644
index 0000000..6e95028
--- /dev/null
+++ b/hooks/charmhelpers/fetch/python/packages.py
@@ -0,0 +1,154 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright 2014-2015 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import os
+import six
+import subprocess
+import sys
+
+from charmhelpers.fetch import apt_install, apt_update
+from charmhelpers.core.hookenv import charm_dir, log
+
+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
+
+
+def pip_execute(*args, **kwargs):
+ """Overriden pip_execute() to stop sys.path being changed.
+
+ The act of importing main from the pip module seems to cause add wheels
+ from the /usr/share/python-wheels which are installed by various tools.
+ This function ensures that sys.path remains the same after the call is
+ executed.
+ """
+ try:
+ _path = sys.path
+ try:
+ from pip import main as _pip_execute
+ except ImportError:
+ apt_update()
+ if six.PY2:
+ apt_install('python-pip')
+ else:
+ apt_install('python3-pip')
+ from pip import main as _pip_execute
+ _pip_execute(*args, **kwargs)
+ finally:
+ sys.path = _path
+
+
+def parse_options(given, available):
+ """Given a set of options, check if available"""
+ for key, value in sorted(given.items()):
+ if not value:
+ continue
+ if key in available:
+ yield "--{0}={1}".format(key, value)
+
+
+def pip_install_requirements(requirements, constraints=None, **options):
+ """Install a requirements file.
+
+ :param constraints: Path to pip constraints file.
+ http://pip.readthedocs.org/en/stable/user_guide/#constraints-files
+ """
+ command = ["install"]
+
+ available_options = ('proxy', 'src', 'log', )
+ for option in parse_options(options, available_options):
+ command.append(option)
+
+ command.append("-r {0}".format(requirements))
+ if constraints:
+ command.append("-c {0}".format(constraints))
+ log("Installing from file: {} with constraints {} "
+ "and options: {}".format(requirements, constraints, command))
+ else:
+ log("Installing from file: {} with options: {}".format(requirements,
+ command))
+ pip_execute(command)
+
+
+def pip_install(package, fatal=False, upgrade=False, venv=None,
+ constraints=None, **options):
+ """Install a python package"""
+ if venv:
+ venv_python = os.path.join(venv, 'bin/pip')
+ command = [venv_python, "install"]
+ else:
+ command = ["install"]
+
+ available_options = ('proxy', 'src', 'log', 'index-url', )
+ for option in parse_options(options, available_options):
+ command.append(option)
+
+ if upgrade:
+ command.append('--upgrade')
+
+ if constraints:
+ command.extend(['-c', constraints])
+
+ if isinstance(package, list):
+ command.extend(package)
+ else:
+ command.append(package)
+
+ log("Installing {} package with options: {}".format(package,
+ command))
+ if venv:
+ subprocess.check_call(command)
+ else:
+ pip_execute(command)
+
+
+def pip_uninstall(package, **options):
+ """Uninstall a python package"""
+ command = ["uninstall", "-q", "-y"]
+
+ available_options = ('proxy', 'log', )
+ for option in parse_options(options, available_options):
+ command.append(option)
+
+ if isinstance(package, list):
+ command.extend(package)
+ else:
+ command.append(package)
+
+ log("Uninstalling {} package with options: {}".format(package,
+ command))
+ pip_execute(command)
+
+
+def pip_list():
+ """Returns the list of current python installed packages
+ """
+ return pip_execute(["list"])
+
+
+def pip_create_virtualenv(path=None):
+ """Create an isolated Python environment."""
+ if six.PY2:
+ apt_install('python-virtualenv')
+ else:
+ apt_install('python3-virtualenv')
+
+ if path:
+ venv_path = path
+ else:
+ venv_path = os.path.join(charm_dir(), 'venv')
+
+ if not os.path.exists(venv_path):
+ subprocess.check_call(['virtualenv', venv_path])
diff --git a/hooks/charmhelpers/fetch/python/rpdb.py b/hooks/charmhelpers/fetch/python/rpdb.py
new file mode 100644
index 0000000..9b31610
--- /dev/null
+++ b/hooks/charmhelpers/fetch/python/rpdb.py
@@ -0,0 +1,56 @@
+# Copyright 2014-2015 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Remote Python Debugger (pdb wrapper)."""
+
+import pdb
+import socket
+import sys
+
+__author__ = "Bertrand Janin <b@janin.com>"
+__version__ = "0.1.3"
+
+
+class Rpdb(pdb.Pdb):
+
+ def __init__(self, addr="127.0.0.1", port=4444):
+ """Initialize the socket and initialize pdb."""
+
+ # Backup stdin and stdout before replacing them by the socket handle
+ self.old_stdout = sys.stdout
+ self.old_stdin = sys.stdin
+
+ # Open a 'reusable' socket to let the webapp reload on the same port
+ self.skt = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
+ self.skt.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, True)
+ self.skt.bind((addr, port))
+ self.skt.listen(1)
+ (clientsocket, address) = self.skt.accept()
+ handle = clientsocket.makefile('rw')
+ pdb.Pdb.__init__(self, completekey='tab', stdin=handle, stdout=handle)
+ sys.stdout = sys.stdin = handle
+
+ def shutdown(self):
+ """Revert stdin and stdout, close the socket."""
+ sys.stdout = self.old_stdout
+ sys.stdin = self.old_stdin
+ self.skt.close()
+ self.set_continue()
+
+ def do_continue(self, arg):
+ """Stop all operation on ``continue``."""
+ self.shutdown()
+ return 1
+
+ do_EOF = do_quit = do_exit = do_c = do_cont = do_continue
diff --git a/hooks/charmhelpers/fetch/python/version.py b/hooks/charmhelpers/fetch/python/version.py
new file mode 100644
index 0000000..3eb4210
--- /dev/null
+++ b/hooks/charmhelpers/fetch/python/version.py
@@ -0,0 +1,32 @@
+#!/usr/bin/env python
+# coding: utf-8
+
+# Copyright 2014-2015 Canonical Limited.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import sys
+
+__author__ = "Jorge Niedbalski <jorge.niedbalski@canonical.com>"
+
+
+def current_version():
+ """Current system python version"""
+ return sys.version_info
+
+
+def current_version_string():
+ """Current system python version as string major.minor.micro"""
+ return "{0}.{1}.{2}".format(sys.version_info.major,
+ sys.version_info.minor,
+ sys.version_info.micro)

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