14
import jujuclient.utils
16
from jujuclient.juju1.connector import Connector
17
from jujuclient.juju1.environment import Environment
18
from jujuclient.juju1.rpc import RPC
19
from jujuclient.juju1.watch import WaitForNoMachines
20
from jujuclient.juju1.facades import (
30
from jujuclient.exc import (
32
EnvironmentNotBootstrapped,
34
from jujuclient.connector import SSL_VERSION
36
# skip this entire test module unless we're running on juju 1.x
37
pytestmark = pytest.mark.skipif(
38
jujuclient.utils.get_juju_major_version() != 1,
39
reason="Running Juju 1.x tests only",
43
ssl._create_default_https_context = ssl._create_unverified_context
44
except AttributeError:
45
# Legacy Python doesn't verify by default (see pep-0476)
46
# https://www.python.org/dev/peps/pep-0476/
50
ENV_NAME = os.environ.get("JUJU_TEST_ENV")
53
raise ValueError("No Testing Environment Defined.")
58
'environ-uuid': 'some-uuid',
59
'server-uuid': 'server-uuid',
60
'state-servers': ['localhost:12345'],
61
'ca-cert': 'test-cert',
67
for s in status['Services'].keys():
68
env.destroy_service(s)
70
env.destroy_machines(sorted(status['Machines'].keys())[1:], force=True)
72
env.destroy_machines(sorted(status['Machines'].keys()), force=True)
73
watch = env.get_watch()
74
WaitForNoMachines(watch, status['Machines']).run()
77
if len(status['Services']) == 0:
81
class ClientRPCTest(unittest.TestCase):
83
@mock.patch('jujuclient.juju1.rpc.RPC._send_request')
84
@mock.patch('jujuclient.juju1.rpc.RPC._upgrade_retry_delay_secs', 0.001)
85
def test_retry_on_upgrade_error(self, send_request):
86
send_request.return_value = {"Error": "upgrade in progress"}
88
rpc_client.conn = mock.Mock()
89
self.assertRaises(EnvError, rpc_client.login, "password")
90
self.assertEquals(send_request.call_count, 61)
92
@mock.patch('jujuclient.juju1.rpc.RPC._send_request')
93
def test_no_retry_required(self, send_request):
94
send_request.return_value = {"Error": "some other error"}
96
rpc_client.conn = mock.Mock()
97
self.assertRaises(EnvError, rpc_client.login, "password")
98
self.assertEquals(send_request.call_count, 1)
101
class ClientConnectorTest(unittest.TestCase):
104
d = tempfile.mkdtemp()
105
self.addCleanup(shutil.rmtree, d)
108
@mock.patch('jujuclient.connector.websocket')
109
def test_connect_socket(self, websocket):
110
address = "wss://abc:17070"
111
Connector.connect_socket(address)
112
websocket.create_connection.assert_called_once_with(
113
address, origin=address, sslopt={
114
'ssl_version': SSL_VERSION,
115
'cert_reqs': ssl.CERT_NONE})
117
@mock.patch('socket.create_connection')
118
def test_is_server_available_unknown_error(self, connect_socket):
119
connect_socket.side_effect = ValueError()
121
ValueError, Connector().is_server_available,
122
'foo.example.com:7070')
124
@mock.patch('socket.create_connection')
125
def test_is_server_available_known_error(self, connect_socket):
127
e.errno = errno.ETIMEDOUT
128
connect_socket.side_effect = e
130
Connector().is_server_available("foo.example.com:7070"))
132
def test_split_host_port_dns(self):
134
Connector.split_host_port('foo.example.com:7070'),
135
('foo.example.com', '7070')
138
def test_is_server_available_ipv4(self):
140
Connector.split_host_port('10.0.0.10:7070'),
141
('10.0.0.10', '7070')
144
def test_is_server_available_ipv6(self):
146
Connector.split_host_port('[2001:db8::1]:7070'),
147
('2001:db8::1', '7070')
150
def test_is_server_available_invalid_input(self):
152
ValueError, Connector.split_host_port, 'I am not an ip/port combo'
155
def test_parse_env_missing(self):
156
temp_juju_home = self.mkdir()
157
with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
158
connector = Connector()
160
EnvironmentNotBootstrapped,
164
def test_parse_env_jenv(self):
165
temp_juju_home = self.mkdir()
166
self.write_jenv(temp_juju_home, 'test-model', SAMPLE_CONFIG)
167
with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
168
connector = Connector()
169
home, env = connector.parse_env('test-model')
170
self.assertEqual(home, temp_juju_home)
171
self.assertEqual(env, SAMPLE_CONFIG)
173
def test_parse_cache_file(self):
174
temp_juju_home = self.mkdir()
175
self.write_cache_file(temp_juju_home, 'test-model', SAMPLE_CONFIG)
176
with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
177
connector = Connector()
178
home, env = connector.parse_env('test-model')
179
self.assertEqual(home, temp_juju_home)
180
self.assertEqual(env, SAMPLE_CONFIG)
182
def test_parse_cache_file_missing_env(self):
183
"""Create a valid cache file, but look for an environment that isn't
186
temp_juju_home = self.mkdir()
187
self.write_cache_file(temp_juju_home, 'test-model', SAMPLE_CONFIG)
188
with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
189
connector = Connector()
191
EnvironmentNotBootstrapped,
195
def test_parse_env_cache_file_first(self):
196
"""The cache file has priority over a jenv file."""
197
temp_juju_home = self.mkdir()
198
content = copy.deepcopy(SAMPLE_CONFIG)
199
self.write_jenv(temp_juju_home, 'test-model', content)
200
# Now change the password.
201
content['password'] = 'new password'
202
self.write_cache_file(temp_juju_home, 'test-model', content)
203
with mock.patch.dict('os.environ', {'JUJU_HOME': temp_juju_home}):
204
connector = Connector()
205
home, env = connector.parse_env('test-model')
206
self.assertEqual(home, temp_juju_home)
207
self.assertEqual(env, content)
209
def write_jenv(self, juju_home, env_name, content):
210
env_dir = os.path.join(juju_home, 'environments')
211
if not os.path.exists(env_dir):
213
jenv = os.path.join(env_dir, '%s.jenv' % env_name)
214
with open(jenv, 'w') as f:
215
yaml.dump(content, f, default_flow_style=False)
217
def write_cache_file(self, juju_home, env_name, content):
218
env_dir = os.path.join(juju_home, 'environments')
219
if not os.path.exists(env_dir):
221
filename = os.path.join(env_dir, 'cache.yaml')
224
env_name: {'env-uuid': content['environ-uuid'],
225
'server-uuid': content['server-uuid'],
226
'user': content['user']}},
228
content['server-uuid']: {
229
'api-endpoints': content['state-servers'],
230
'ca-cert': content['ca-cert'],
231
'identities': {content['user']: content['password']}}},
232
# Explicitly don't care about 'server-user' here.
234
with open(filename, 'w') as f:
235
yaml.dump(cache_content, f, default_flow_style=False)
238
class KeyManagerTest(unittest.TestCase):
240
self.env = Environment.connect(ENV_NAME)
241
self.keys = KeyManager(self.env)
246
def verify_keys(self, expected, user='admin', present=True):
247
keys = self.keys(user)['Results'][0]['Result']
256
raise AssertionError("%s not found in %s" % (e, keys))
259
raise AssertionError("%s not found in %s" % (e, keys))
261
@pytest.mark.skipif(True, reason="not implemented")
262
def test_key_manager(self):
263
self.verify_keys(['juju-client-key', 'juju-system-key'])
265
self.key.import_keys('admin', ['hazmat']),
266
{u'Results': [{u'Error': None}]})
267
self.verify_keys(['ssh-import-id lp:hazmat'])
270
'kapil@objectrealms-laptop.local # ssh-import-id lp:hazmat')
271
self.verify_keys(['ssh-import-id lp:hazmat'], present=False)
274
class BackupTest(unittest.TestCase):
276
self.env = Environment.connect(ENV_NAME)
277
self.bm = Backups(self.env)
282
@pytest.mark.skipif(True, reason="broken cleanup")
283
def test_backups(self):
284
assert self.bm.list()['List'] == []
285
info = self.bm.create('abc')
286
assert len(self.bm.list()['List']) == 2
287
assert self.bm.info(info['ID'])['Notes'] == 'abc'
288
self.bm.remove(info['ID'])
289
assert len(self.bm.list()['List']) == []
292
class UserManagerTest(unittest.TestCase):
295
self.env = Environment.connect(ENV_NAME)
296
self.um = UserManager(self.env)
301
def assert_user(self, user):
302
result = self.um.info(user['username'])
303
result = result['results'][0]['result']
304
result.pop('date-created')
305
self.assertEqual(result, user)
307
@pytest.mark.skipif(True, reason="broken cleanup")
308
def test_user_manager(self):
309
result = self.um.add(
310
{'username': 'magicmike', 'display-name': 'zerocool',
311
'password': 'guess'})
312
assert result == {'results': [{'tag': 'user-magicmike@local'}]}
314
'username': 'magicmike',
316
'display-name': 'zerocool',
317
'created-by': 'admin@local'})
318
self.um.disable('mike')
320
'username': 'magicmike',
322
'display-name': 'zerocool',
323
'created-by': 'admin@local'})
324
self.um.enable('mike')
326
'username': 'magicmike',
328
'display-name': 'zerocool',
329
'created-by': 'admin@local'})
331
self.um.set_password({'username': 'mike', 'password': 'iforgot'}),
332
{u'Results': [{u'Error': None}]})
333
self.um.disable('mike')
336
class CharmBase(object):
342
if not self._repo_dir:
343
self._repo_dir = self.mkdir()
344
return self._repo_dir
347
d = tempfile.mkdtemp()
348
self.addCleanup(shutil.rmtree, d)
351
def write_local_charm(self, md, config=None, actions=None):
352
charm_dir = os.path.join(self.repo_dir, md['series'], md['name'])
353
if not os.path.exists(charm_dir):
354
os.makedirs(charm_dir)
355
md_path = os.path.join(charm_dir, 'metadata.yaml')
356
with open(md_path, 'w') as fh:
357
md.pop('series', None)
358
fh.write(yaml.safe_dump(md))
360
if config is not None:
361
cfg_path = os.path.join(charm_dir, 'config.yaml')
362
with open(cfg_path, 'w') as fh:
363
fh.write(yaml.safe_dump(config))
365
if actions is not None:
366
act_path = os.path.join(charm_dir, 'actions.yaml')
367
with open(act_path, 'w') as fh:
368
fh.write(yaml.safe_dump(actions))
370
with open(os.path.join(charm_dir, 'revision'), 'w') as fh:
374
class ActionTest(unittest.TestCase, CharmBase):
377
self.env = Environment.connect(ENV_NAME)
378
self.actions = Actions(self.env)
384
def setupCharm(self):
387
'description': 'does something with six',
392
self.write_local_charm({
394
'summary': 'its a db',
395
'description': 'for storing things',
399
'interface': 'mysql'}}}, actions=actions)
401
@pytest.mark.skipif(True, reason="broken test call to api")
402
def test_actions(self):
403
result = self.env.add_local_charm_dir(
404
os.path.join(self.repo_dir, 'trusty', 'mysql'),
406
charm_url = result['CharmURL']
407
self.env.deploy('action-db', charm_url)
408
actions = self.actions.service_actions('action-db')
412
{u'servicetag': u'service-action-db',
413
u'actions': {u'ActionSpecs':
415
u'Params': {u'title': u'deepsix',
418
u'does something with six',
422
u'type': u'string'}}},
423
u'Description': u'does something with six'
425
result = self.actions.enqueue_units(
426
'action-db/0', 'deepsix', {'optiona': 'bez'})
427
self.assertEqual(result, [])
430
class CharmTest(unittest.TestCase):
433
self.env = Environment.connect(ENV_NAME)
434
self.charms = Charms(self.env)
439
def test_charm(self):
441
self.env.add_charm('cs:~hazmat/trusty/etcd-6')
443
self.charms.info('cs:~hazmat/trusty/etcd-6')
446
class HATest(unittest.TestCase):
449
self.env = Environment.connect(ENV_NAME)
450
self.ha = HA(self.env)
455
@pytest.mark.skipif(True, reason="incomplete implementation issue")
457
previous = self.env.status()
458
self.ha.ensure_availability(3)
459
current = self.env.status()
460
self.assertNotEqual(previous, current)
463
class AnnotationTest(unittest.TestCase):
466
self.env = Environment.connect(ENV_NAME)
467
self.charms = Annotations(self.env)
473
class ClientTest(unittest.TestCase):
476
self.env = Environment.connect(ENV_NAME)
482
def destroy_service(self, svc):
483
self.env.destroy_service(svc)
485
if svc not in self.env.status().get('Services', {}):
488
def assert_service(self, svc_name, num_units=None):
489
status = self.env.status()
490
services = status.get('Services', {})
492
svc_name in services,
493
"Service {} does not exist".format(svc_name)
495
if num_units is not None:
496
count = len(services[svc_name]['Units'])
499
"Service {} has {} units, expected {}".format(
500
svc_name, count, num_units)
503
def assert_not_service(self, svc_name):
504
status = self.env.status()
505
services = status.get('Services', {})
506
if svc_name in services:
508
services[svc_name]['Life'] in ('dying', 'dead'))
510
def test_juju_info(self):
511
info_keys = list(sorted(self.env.info().keys()))
513
'DefaultSeries', 'Name', 'ProviderType', 'ServerUUID', 'UUID']
514
assert info_keys == control
516
def test_add_get_charm(self):
517
self.env.add_charm('cs:~hazmat/trusty/etcd-6')
518
charm = self.env.get_charm(
519
'cs:~hazmat/trusty/etcd-6')
520
assert charm['URL'] == 'cs:~hazmat/trusty/etcd-6'
522
def test_add_local_charm(self):
523
with tempfile.NamedTemporaryFile() as f:
525
EnvError, self.env.add_local_charm, f.name, 'trusty')
527
def test_deploy_and_destroy(self):
528
self.assert_not_service('db')
529
self.env.deploy('db', 'cs:trusty/mysql-1')
530
self.assert_service('db')
531
self.destroy_service('db')
532
self.assert_not_service('db')
534
def xtest_expose_unexpose(self):
537
def test_add_remove_units(self):
538
self.assert_not_service('db')
539
machine_1 = self.env.add_machine(series="trusty")['Machine']
540
machine_2 = self.env.add_machine(series="trusty")['Machine']
541
self.env.deploy('db', 'cs:trusty/mysql-1', machine_spec=machine_1)
542
self.env.add_unit('db', machine_spec=machine_2)
543
self.assert_service('db', num_units=2)
544
services = self.env.status().get('Services', {})
545
# Remove the first unit
546
remove_unit = list(services['db']['Units'].keys())[0]
547
self.env.remove_units([remove_unit])
548
self.assert_service('db', num_units=1)
549
self.destroy_service('db')
550
self.assert_not_service('db')
552
def test_deploy_and_add_unit_lxc(self):
553
self.assert_not_service('db')
554
machine = self.env.add_machine(series="trusty")['Machine']
555
self.env.deploy('db', 'cs:trusty/mysql-1', machine_spec=machine)
556
self.env.add_unit('db', machine_spec='lxc:{}'.format(machine))
557
self.assert_service('db', num_units=2)
558
self.destroy_service('db')
559
self.assert_not_service('db')
561
def xtest_get_set_config(self):
564
def test_get_set_constraints(self):
565
self.assert_not_service('db')
566
in_constraints = {'cpu-cores': '2'}
567
self.env.deploy('db', 'cs:trusty/mysql-1', constraints=in_constraints)
568
self.assert_service('db')
569
out_constraints = self.env.get_constraints('db')
570
self.assertEqual(in_constraints, out_constraints)
571
self.destroy_service('db')
572
self.assert_not_service('db')
574
def test_get_set_annotations(self):
575
machine = self.env.add_machine(series="trusty")['Machine']
576
in_annotation = {'foo': 'bar'}
577
self.env.set_annotation(machine, 'machine', in_annotation)
578
out_annotation = self.env.get_annotation(machine, 'machine')
579
self.assertEqual(in_annotation, out_annotation['Annotations'])
581
def xtest_add_remove_relation(self):
584
def xtest_status(self):
587
def xtest_info(self):
590
def test_deploy_and_destroy_placement_machine(self):
591
self.assert_not_service('db')
592
machine = self.env.add_machine(series="trusty")['Machine']
593
self.env.deploy('db', 'cs:trusty/mysql-1', machine_spec=machine)
594
self.assert_service('db')
595
self.destroy_service('db')
596
self.assert_not_service('db')
598
def test_deploy_and_destroy_placement_lxc(self):
599
self.assert_not_service('db')
600
machine = self.env.add_machine(series="trusty")['Machine']
601
machine_spec = 'lxc:{}'.format(machine)
602
self.env.deploy('db', 'cs:trusty/mysql-1', machine_spec=machine_spec)
603
self.assert_service('db')
604
self.destroy_service('db')
605
self.assert_not_service('db')
608
class TestEnvironment(unittest.TestCase):
611
self.env = Environment.connect(ENV_NAME)
613
def test_make_headers(self):
614
headers = self.env._make_headers()
615
self.assertTrue('Authorization' in headers)
617
def test_run_no_target(self):
618
self.assertRaises(AssertionError, self.env.run, "sudo test")
620
def test_run_target_machines(self):
621
with mock.patch.object(self.env, '_rpc',
622
return_value=None) as rpc:
623
self.env.run("sudo test", machines=["0", "1"])
625
rpc.assert_called_once_with({
630
"Commands": "sudo test",
639
def test_run_target_services(self):
640
with mock.patch.object(self.env, '_rpc',
641
return_value=None) as rpc:
642
self.env.run("sudo test", services=["cinder", "glance"])
644
rpc.assert_called_once_with({
649
"Commands": "sudo test",
658
def test_run_target_units(self):
659
with mock.patch.object(self.env, '_rpc',
660
return_value=None) as rpc:
661
self.env.run("sudo test", units=["mysql/0", "mysql/1"])
663
rpc.assert_called_once_with({
668
"Commands": "sudo test",
678
if __name__ == '__main__':