summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorDavid Ames <david.ames@canonical.com>2018-05-08 12:00:51 -0700
committerDavid Ames <david.ames@canonical.com>2018-05-11 16:12:20 -0700
commitf03ccf02b75e27f29708eedc5c236b99f84a5e19 (patch)
treef652c806058d68deeba3a27453a9c1fe98aaa3fc
parentd6cf5285c196e178c4369691c908788d4d8f7954 (diff)
Enable Bionic as a gate test
Change bionic test from dev to gate for 18.05. Change-Id: I5a82ac79b29181fabec41570cd5aa003d2bfb8ea
Notes
Notes (review): Verified+1: Canonical CI <uosci-testing-bot@ubuntu.com> Code-Review+2: James Page <james.page@canonical.com> Workflow+1: James Page <james.page@canonical.com> Verified+2: Zuul Submitted-by: Zuul Submitted-at: Mon, 14 May 2018 11:11:55 +0000 Reviewed-on: https://review.openstack.org/566999 Project: openstack/charm-nova-cloud-controller Branch: refs/heads/master
-rw-r--r--hooks/charmhelpers/contrib/openstack/context.py9
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/section-oslo-middleware5
-rw-r--r--hooks/charmhelpers/contrib/openstack/templates/section-oslo-notifications3
-rw-r--r--hooks/charmhelpers/contrib/openstack/utils.py2
-rw-r--r--hooks/charmhelpers/contrib/openstack/vaultlocker.py126
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/ceph.py43
-rw-r--r--hooks/charmhelpers/contrib/storage/linux/utils.py16
-rw-r--r--hooks/charmhelpers/core/hookenv.py52
-rw-r--r--hooks/charmhelpers/core/services/base.py4
-rw-r--r--hooks/charmhelpers/core/sysctl.py18
-rw-r--r--hooks/charmhelpers/core/unitdata.py9
-rwxr-xr-xtests/gate-basic-bionic-queens (renamed from tests/dev-basic-bionic-queens)0
-rw-r--r--tox.ini2
13 files changed, 256 insertions, 33 deletions
diff --git a/hooks/charmhelpers/contrib/openstack/context.py b/hooks/charmhelpers/contrib/openstack/context.py
index 6c4497b..2d91f0a 100644
--- a/hooks/charmhelpers/contrib/openstack/context.py
+++ b/hooks/charmhelpers/contrib/openstack/context.py
@@ -797,9 +797,9 @@ class ApacheSSLContext(OSContextGenerator):
797 key_filename = 'key' 797 key_filename = 'key'
798 798
799 write_file(path=os.path.join(ssl_dir, cert_filename), 799 write_file(path=os.path.join(ssl_dir, cert_filename),
800 content=b64decode(cert)) 800 content=b64decode(cert), perms=0o640)
801 write_file(path=os.path.join(ssl_dir, key_filename), 801 write_file(path=os.path.join(ssl_dir, key_filename),
802 content=b64decode(key)) 802 content=b64decode(key), perms=0o640)
803 803
804 def configure_ca(self): 804 def configure_ca(self):
805 ca_cert = get_ca_cert() 805 ca_cert = get_ca_cert()
@@ -1873,10 +1873,11 @@ class EnsureDirContext(OSContextGenerator):
1873 context is needed to do that before rendering a template. 1873 context is needed to do that before rendering a template.
1874 ''' 1874 '''
1875 1875
1876 def __init__(self, dirname): 1876 def __init__(self, dirname, **kwargs):
1877 '''Used merely to ensure that a given directory exists.''' 1877 '''Used merely to ensure that a given directory exists.'''
1878 self.dirname = dirname 1878 self.dirname = dirname
1879 self.kwargs = kwargs
1879 1880
1880 def __call__(self): 1881 def __call__(self):
1881 mkdir(self.dirname) 1882 mkdir(self.dirname, **self.kwargs)
1882 return {} 1883 return {}
diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-oslo-middleware b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-middleware
new file mode 100644
index 0000000..dd73230
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-middleware
@@ -0,0 +1,5 @@
1[oslo_middleware]
2
3# Bug #1758675
4enable_proxy_headers_parsing = true
5
diff --git a/hooks/charmhelpers/contrib/openstack/templates/section-oslo-notifications b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-notifications
index 5dccd4b..021a3c2 100644
--- a/hooks/charmhelpers/contrib/openstack/templates/section-oslo-notifications
+++ b/hooks/charmhelpers/contrib/openstack/templates/section-oslo-notifications
@@ -5,4 +5,7 @@ transport_url = {{ transport_url }}
5{% if notification_topics -%} 5{% if notification_topics -%}
6topics = {{ notification_topics }} 6topics = {{ notification_topics }}
7{% endif -%} 7{% endif -%}
8{% if notification_format -%}
9notification_format = {{ notification_format }}
10{% endif -%}
8{% endif -%} 11{% endif -%}
diff --git a/hooks/charmhelpers/contrib/openstack/utils.py b/hooks/charmhelpers/contrib/openstack/utils.py
index e719426..6184abd 100644
--- a/hooks/charmhelpers/contrib/openstack/utils.py
+++ b/hooks/charmhelpers/contrib/openstack/utils.py
@@ -306,7 +306,7 @@ def get_os_codename_install_source(src):
306 306
307 if src.startswith('cloud:'): 307 if src.startswith('cloud:'):
308 ca_rel = src.split(':')[1] 308 ca_rel = src.split(':')[1]
309 ca_rel = ca_rel.split('%s-' % ubuntu_rel)[1].split('/')[0] 309 ca_rel = ca_rel.split('-')[1].split('/')[0]
310 return ca_rel 310 return ca_rel
311 311
312 # Best guess match based on deb string provided 312 # Best guess match based on deb string provided
diff --git a/hooks/charmhelpers/contrib/openstack/vaultlocker.py b/hooks/charmhelpers/contrib/openstack/vaultlocker.py
new file mode 100644
index 0000000..a8e4bf8
--- /dev/null
+++ b/hooks/charmhelpers/contrib/openstack/vaultlocker.py
@@ -0,0 +1,126 @@
1# Copyright 2018 Canonical Limited.
2#
3# Licensed under the Apache License, Version 2.0 (the "License");
4# you may not use this file except in compliance with the License.
5# You may obtain a copy of the License at
6#
7# http://www.apache.org/licenses/LICENSE-2.0
8#
9# Unless required by applicable law or agreed to in writing, software
10# distributed under the License is distributed on an "AS IS" BASIS,
11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12# See the License for the specific language governing permissions and
13# limitations under the License.
14
15import json
16import os
17
18import charmhelpers.contrib.openstack.alternatives as alternatives
19import charmhelpers.contrib.openstack.context as context
20
21import charmhelpers.core.hookenv as hookenv
22import charmhelpers.core.host as host
23import charmhelpers.core.templating as templating
24import charmhelpers.core.unitdata as unitdata
25
26VAULTLOCKER_BACKEND = 'charm-vaultlocker'
27
28
29class VaultKVContext(context.OSContextGenerator):
30 """Vault KV context for interaction with vault-kv interfaces"""
31 interfaces = ['secrets-storage']
32
33 def __init__(self, secret_backend=None):
34 super(context.OSContextGenerator, self).__init__()
35 self.secret_backend = (
36 secret_backend or 'charm-{}'.format(hookenv.service_name())
37 )
38
39 def __call__(self):
40 db = unitdata.kv()
41 last_token = db.get('last-token')
42 secret_id = db.get('secret-id')
43 for relation_id in hookenv.relation_ids(self.interfaces[0]):
44 for unit in hookenv.related_units(relation_id):
45 data = hookenv.relation_get(unit=unit,
46 rid=relation_id)
47 vault_url = data.get('vault_url')
48 role_id = data.get('{}_role_id'.format(hookenv.local_unit()))
49 token = data.get('{}_token'.format(hookenv.local_unit()))
50
51 if all([vault_url, role_id, token]):
52 token = json.loads(token)
53 vault_url = json.loads(vault_url)
54
55 # Tokens may change when secret_id's are being
56 # reissued - if so use token to get new secret_id
57 if token != last_token:
58 secret_id = retrieve_secret_id(
59 url=vault_url,
60 token=token
61 )
62 db.set('secret-id', secret_id)
63 db.set('last-token', token)
64 db.flush()
65
66 ctxt = {
67 'vault_url': vault_url,
68 'role_id': json.loads(role_id),
69 'secret_id': secret_id,
70 'secret_backend': self.secret_backend,
71 }
72 vault_ca = data.get('vault_ca')
73 if vault_ca:
74 ctxt['vault_ca'] = json.loads(vault_ca)
75 self.complete = True
76 return ctxt
77 return {}
78
79
80def write_vaultlocker_conf(context, priority=100):
81 """Write vaultlocker configuration to disk and install alternative
82
83 :param context: Dict of data from vault-kv relation
84 :ptype: context: dict
85 :param priority: Priority of alternative configuration
86 :ptype: priority: int"""
87 charm_vl_path = "/var/lib/charm/{}/vaultlocker.conf".format(
88 hookenv.service_name()
89 )
90 host.mkdir(os.path.dirname(charm_vl_path), perms=0o700)
91 templating.render(source='vaultlocker.conf.j2',
92 target=charm_vl_path,
93 context=context, perms=0o600),
94 alternatives.install_alternative('vaultlocker.conf',
95 '/etc/vaultlocker/vaultlocker.conf',
96 charm_vl_path, priority)
97
98
99def vault_relation_complete(backend=None):
100 """Determine whether vault relation is complete
101
102 :param backend: Name of secrets backend requested
103 :ptype backend: string
104 :returns: whether the relation to vault is complete
105 :rtype: bool"""
106 vault_kv = VaultKVContext(secret_backend=backend or VAULTLOCKER_BACKEND)
107 vault_kv()
108 return vault_kv.complete
109
110
111# TODO: contrib a high level unwrap method to hvac that works
112def retrieve_secret_id(url, token):
113 """Retrieve a response-wrapped secret_id from Vault
114
115 :param url: URL to Vault Server
116 :ptype url: str
117 :param token: One shot Token to use
118 :ptype token: str
119 :returns: secret_id to use for Vault Access
120 :rtype: str"""
121 import hvac
122 client = hvac.Client(url=url, token=token)
123 response = client._post('/v1/sys/wrapping/unwrap')
124 if response.status_code == 200:
125 data = response.json()
126 return data['data']['secret_id']
diff --git a/hooks/charmhelpers/contrib/storage/linux/ceph.py b/hooks/charmhelpers/contrib/storage/linux/ceph.py
index e13e60a..7682820 100644
--- a/hooks/charmhelpers/contrib/storage/linux/ceph.py
+++ b/hooks/charmhelpers/contrib/storage/linux/ceph.py
@@ -291,7 +291,7 @@ class Pool(object):
291 291
292class ReplicatedPool(Pool): 292class ReplicatedPool(Pool):
293 def __init__(self, service, name, pg_num=None, replicas=2, 293 def __init__(self, service, name, pg_num=None, replicas=2,
294 percent_data=10.0): 294 percent_data=10.0, app_name=None):
295 super(ReplicatedPool, self).__init__(service=service, name=name) 295 super(ReplicatedPool, self).__init__(service=service, name=name)
296 self.replicas = replicas 296 self.replicas = replicas
297 if pg_num: 297 if pg_num:
@@ -301,6 +301,10 @@ class ReplicatedPool(Pool):
301 self.pg_num = min(pg_num, max_pgs) 301 self.pg_num = min(pg_num, max_pgs)
302 else: 302 else:
303 self.pg_num = self.get_pgs(self.replicas, percent_data) 303 self.pg_num = self.get_pgs(self.replicas, percent_data)
304 if app_name:
305 self.app_name = app_name
306 else:
307 self.app_name = 'unknown'
304 308
305 def create(self): 309 def create(self):
306 if not pool_exists(self.service, self.name): 310 if not pool_exists(self.service, self.name):
@@ -313,6 +317,12 @@ class ReplicatedPool(Pool):
313 update_pool(client=self.service, 317 update_pool(client=self.service,
314 pool=self.name, 318 pool=self.name,
315 settings={'size': str(self.replicas)}) 319 settings={'size': str(self.replicas)})
320 try:
321 set_app_name_for_pool(client=self.service,
322 pool=self.name,
323 name=self.app_name)
324 except CalledProcessError:
325 log('Could not set app name for pool {}'.format(self.name, level=WARNING))
316 except CalledProcessError: 326 except CalledProcessError:
317 raise 327 raise
318 328
@@ -320,10 +330,14 @@ class ReplicatedPool(Pool):
320# Default jerasure erasure coded pool 330# Default jerasure erasure coded pool
321class ErasurePool(Pool): 331class ErasurePool(Pool):
322 def __init__(self, service, name, erasure_code_profile="default", 332 def __init__(self, service, name, erasure_code_profile="default",
323 percent_data=10.0): 333 percent_data=10.0, app_name=None):
324 super(ErasurePool, self).__init__(service=service, name=name) 334 super(ErasurePool, self).__init__(service=service, name=name)
325 self.erasure_code_profile = erasure_code_profile 335 self.erasure_code_profile = erasure_code_profile
326 self.percent_data = percent_data 336 self.percent_data = percent_data
337 if app_name:
338 self.app_name = app_name
339 else:
340 self.app_name = 'unknown'
327 341
328 def create(self): 342 def create(self):
329 if not pool_exists(self.service, self.name): 343 if not pool_exists(self.service, self.name):
@@ -355,6 +369,12 @@ class ErasurePool(Pool):
355 'erasure', self.erasure_code_profile] 369 'erasure', self.erasure_code_profile]
356 try: 370 try:
357 check_call(cmd) 371 check_call(cmd)
372 try:
373 set_app_name_for_pool(client=self.service,
374 pool=self.name,
375 name=self.app_name)
376 except CalledProcessError:
377 log('Could not set app name for pool {}'.format(self.name, level=WARNING))
358 except CalledProcessError: 378 except CalledProcessError:
359 raise 379 raise
360 380
@@ -778,6 +798,25 @@ def update_pool(client, pool, settings):
778 check_call(cmd) 798 check_call(cmd)
779 799
780 800
801def set_app_name_for_pool(client, pool, name):
802 """
803 Calls `osd pool application enable` for the specified pool name
804
805 :param client: Name of the ceph client to use
806 :type client: str
807 :param pool: Pool to set app name for
808 :type pool: str
809 :param name: app name for the specified pool
810 :type name: str
811
812 :raises: CalledProcessError if ceph call fails
813 """
814 if ceph_version() >= '12.0.0':
815 cmd = ['ceph', '--id', client, 'osd', 'pool',
816 'application', 'enable', pool, name]
817 check_call(cmd)
818
819
781def create_pool(service, name, replicas=3, pg_num=None): 820def create_pool(service, name, replicas=3, pg_num=None):
782 """Create a new RADOS pool.""" 821 """Create a new RADOS pool."""
783 if pool_exists(service, name): 822 if pool_exists(service, name):
diff --git a/hooks/charmhelpers/contrib/storage/linux/utils.py b/hooks/charmhelpers/contrib/storage/linux/utils.py
index c942889..6f846b0 100644
--- a/hooks/charmhelpers/contrib/storage/linux/utils.py
+++ b/hooks/charmhelpers/contrib/storage/linux/utils.py
@@ -67,3 +67,19 @@ def is_device_mounted(device):
67 except Exception: 67 except Exception:
68 return False 68 return False
69 return bool(re.search(r'MOUNTPOINT=".+"', out)) 69 return bool(re.search(r'MOUNTPOINT=".+"', out))
70
71
72def mkfs_xfs(device, force=False):
73 """Format device with XFS filesystem.
74
75 By default this should fail if the device already has a filesystem on it.
76 :param device: Full path to device to format
77 :ptype device: tr
78 :param force: Force operation
79 :ptype: force: boolean"""
80 cmd = ['mkfs.xfs']
81 if force:
82 cmd.append("-f")
83
84 cmd += ['-i', 'size=1024', device]
85 check_call(cmd)
diff --git a/hooks/charmhelpers/core/hookenv.py b/hooks/charmhelpers/core/hookenv.py
index 89f1024..627d8f7 100644
--- a/hooks/charmhelpers/core/hookenv.py
+++ b/hooks/charmhelpers/core/hookenv.py
@@ -290,7 +290,7 @@ class Config(dict):
290 self.implicit_save = True 290 self.implicit_save = True
291 self._prev_dict = None 291 self._prev_dict = None
292 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME) 292 self.path = os.path.join(charm_dir(), Config.CONFIG_FILE_NAME)
293 if os.path.exists(self.path): 293 if os.path.exists(self.path) and os.stat(self.path).st_size:
294 self.load_previous() 294 self.load_previous()
295 atexit(self._implicit_save) 295 atexit(self._implicit_save)
296 296
@@ -310,7 +310,11 @@ class Config(dict):
310 """ 310 """
311 self.path = path or self.path 311 self.path = path or self.path
312 with open(self.path) as f: 312 with open(self.path) as f:
313 self._prev_dict = json.load(f) 313 try:
314 self._prev_dict = json.load(f)
315 except ValueError as e:
316 log('Unable to parse previous config data - {}'.format(str(e)),
317 level=ERROR)
314 for k, v in copy.deepcopy(self._prev_dict).items(): 318 for k, v in copy.deepcopy(self._prev_dict).items():
315 if k not in self: 319 if k not in self:
316 self[k] = v 320 self[k] = v
@@ -354,22 +358,40 @@ class Config(dict):
354 self.save() 358 self.save()
355 359
356 360
357@cached 361_cache_config = None
362
363
358def config(scope=None): 364def config(scope=None):
359 """Juju charm configuration""" 365 """
360 config_cmd_line = ['config-get'] 366 Get the juju charm configuration (scope==None) or individual key,
361 if scope is not None: 367 (scope=str). The returned value is a Python data structure loaded as
362 config_cmd_line.append(scope) 368 JSON from the Juju config command.
363 else: 369
364 config_cmd_line.append('--all') 370 :param scope: If set, return the value for the specified key.
365 config_cmd_line.append('--format=json') 371 :type scope: Optional[str]
372 :returns: Either the whole config as a Config, or a key from it.
373 :rtype: Any
374 """
375 global _cache_config
376 config_cmd_line = ['config-get', '--all', '--format=json']
366 try: 377 try:
367 config_data = json.loads( 378 # JSON Decode Exception for Python3.5+
368 subprocess.check_output(config_cmd_line).decode('UTF-8')) 379 exc_json = json.decoder.JSONDecodeError
380 except AttributeError:
381 # JSON Decode Exception for Python2.7 through Python3.4
382 exc_json = ValueError
383 try:
384 if _cache_config is None:
385 config_data = json.loads(
386 subprocess.check_output(config_cmd_line).decode('UTF-8'))
387 _cache_config = Config(config_data)
369 if scope is not None: 388 if scope is not None:
370 return config_data 389 return _cache_config.get(scope)
371 return Config(config_data) 390 return _cache_config
372 except ValueError: 391 except (exc_json, UnicodeDecodeError) as e:
392 log('Unable to parse output from config-get: config_cmd_line="{}" '
393 'message="{}"'
394 .format(config_cmd_line, str(e)), level=ERROR)
373 return None 395 return None
374 396
375 397
diff --git a/hooks/charmhelpers/core/services/base.py b/hooks/charmhelpers/core/services/base.py
index 345b60d..179ad4f 100644
--- a/hooks/charmhelpers/core/services/base.py
+++ b/hooks/charmhelpers/core/services/base.py
@@ -307,7 +307,9 @@ class PortManagerCallback(ManagerCallback):
307 """ 307 """
308 def __call__(self, manager, service_name, event_name): 308 def __call__(self, manager, service_name, event_name):
309 service = manager.get_service(service_name) 309 service = manager.get_service(service_name)
310 new_ports = service.get('ports', []) 310 # turn this generator into a list,
311 # as we'll be going over it multiple times
312 new_ports = list(service.get('ports', []))
311 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name)) 313 port_file = os.path.join(hookenv.charm_dir(), '.{}.ports'.format(service_name))
312 if os.path.exists(port_file): 314 if os.path.exists(port_file):
313 with open(port_file) as fp: 315 with open(port_file) as fp:
diff --git a/hooks/charmhelpers/core/sysctl.py b/hooks/charmhelpers/core/sysctl.py
index 6e413e3..1f188d8 100644
--- a/hooks/charmhelpers/core/sysctl.py
+++ b/hooks/charmhelpers/core/sysctl.py
@@ -31,18 +31,22 @@ __author__ = 'Jorge Niedbalski R. <jorge.niedbalski@canonical.com>'
31def create(sysctl_dict, sysctl_file): 31def create(sysctl_dict, sysctl_file):
32 """Creates a sysctl.conf file from a YAML associative array 32 """Creates a sysctl.conf file from a YAML associative array
33 33
34 :param sysctl_dict: a YAML-formatted string of sysctl options eg "{ 'kernel.max_pid': 1337 }" 34 :param sysctl_dict: a dict or YAML-formatted string of sysctl
35 options eg "{ 'kernel.max_pid': 1337 }"
35 :type sysctl_dict: str 36 :type sysctl_dict: str
36 :param sysctl_file: path to the sysctl file to be saved 37 :param sysctl_file: path to the sysctl file to be saved
37 :type sysctl_file: str or unicode 38 :type sysctl_file: str or unicode
38 :returns: None 39 :returns: None
39 """ 40 """
40 try: 41 if type(sysctl_dict) is not dict:
41 sysctl_dict_parsed = yaml.safe_load(sysctl_dict) 42 try:
42 except yaml.YAMLError: 43 sysctl_dict_parsed = yaml.safe_load(sysctl_dict)
43 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict), 44 except yaml.YAMLError:
44 level=ERROR) 45 log("Error parsing YAML sysctl_dict: {}".format(sysctl_dict),
45 return 46 level=ERROR)
47 return
48 else:
49 sysctl_dict_parsed = sysctl_dict
46 50
47 with open(sysctl_file, "w") as fd: 51 with open(sysctl_file, "w") as fd:
48 for key, value in sysctl_dict_parsed.items(): 52 for key, value in sysctl_dict_parsed.items():
diff --git a/hooks/charmhelpers/core/unitdata.py b/hooks/charmhelpers/core/unitdata.py
index 6d7b494..ab55432 100644
--- a/hooks/charmhelpers/core/unitdata.py
+++ b/hooks/charmhelpers/core/unitdata.py
@@ -166,6 +166,10 @@ class Storage(object):
166 166
167 To support dicts, lists, integer, floats, and booleans values 167 To support dicts, lists, integer, floats, and booleans values
168 are automatically json encoded/decoded. 168 are automatically json encoded/decoded.
169
170 Note: to facilitate unit testing, ':memory:' can be passed as the
171 path parameter which causes sqlite3 to only build the db in memory.
172 This should only be used for testing purposes.
169 """ 173 """
170 def __init__(self, path=None): 174 def __init__(self, path=None):
171 self.db_path = path 175 self.db_path = path
@@ -175,8 +179,9 @@ class Storage(object):
175 else: 179 else:
176 self.db_path = os.path.join( 180 self.db_path = os.path.join(
177 os.environ.get('CHARM_DIR', ''), '.unit-state.db') 181 os.environ.get('CHARM_DIR', ''), '.unit-state.db')
178 with open(self.db_path, 'a') as f: 182 if self.db_path != ':memory:':
179 os.fchmod(f.fileno(), 0o600) 183 with open(self.db_path, 'a') as f:
184 os.fchmod(f.fileno(), 0o600)
180 self.conn = sqlite3.connect('%s' % self.db_path) 185 self.conn = sqlite3.connect('%s' % self.db_path)
181 self.cursor = self.conn.cursor() 186 self.cursor = self.conn.cursor()
182 self.revision = None 187 self.revision = None
diff --git a/tests/dev-basic-bionic-queens b/tests/gate-basic-bionic-queens
index df820fc..df820fc 100755
--- a/tests/dev-basic-bionic-queens
+++ b/tests/gate-basic-bionic-queens
diff --git a/tox.ini b/tox.ini
index 4319064..09ca045 100644
--- a/tox.ini
+++ b/tox.ini
@@ -60,7 +60,7 @@ basepython = python2.7
60deps = -r{toxinidir}/requirements.txt 60deps = -r{toxinidir}/requirements.txt
61 -r{toxinidir}/test-requirements.txt 61 -r{toxinidir}/test-requirements.txt
62commands = 62commands =
63 bundletester -vl DEBUG -r json -o func-results.json gate-basic-xenial-pike --no-destroy 63 bundletester -vl DEBUG -r json -o func-results.json gate-basic-bionic-queens --no-destroy
64 64
65[testenv:func27-dfs] 65[testenv:func27-dfs]
66# Charm Functional Test 66# Charm Functional Test