blob: 58766b4c5bbd5eb2bae04203355de67830a920a9 [file] [log] [blame]
Adam Israeldcdf82b2017-08-15 15:26:43 -04001import asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -05002import mock
Adam Israeldcdf82b2017-08-15 15:26:43 -04003from concurrent.futures import ThreadPoolExecutor
4from pathlib import Path
Adam Israelb8a82812019-03-27 14:50:11 -04005import paramiko
Adam Israelc3e6c2e2018-03-01 09:31:50 -05006
7from juju.client.client import ConfigValue, ApplicationFacade
8from juju.model import Model, ModelObserver
9from juju.utils import block_until, run_with_interrupt
Adam Israelb0943662018-08-02 15:32:00 -040010from juju.errors import JujuError
11
12import os
13import pylxd
14import time
15import uuid
Adam Israelc3e6c2e2018-03-01 09:31:50 -050016
Adam Israeldcdf82b2017-08-15 15:26:43 -040017import pytest
18
19from .. import base
Adam Israelc3e6c2e2018-03-01 09:31:50 -050020
Adam Israeldcdf82b2017-08-15 15:26:43 -040021
22MB = 1
23GB = 1024
24SSH_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsYMJGNGG74HAJha3n2CFmWYsOOaORnJK6VqNy86pj0MIpvRXBzFzVy09uPQ66GOQhTEoJHEqE77VMui7+62AcMXT+GG7cFHcnU8XVQsGM6UirCcNyWNysfiEMoAdZScJf/GvoY87tMEszhZIUV37z8PUBx6twIqMdr31W1J0IaPa+sV6FEDadeLaNTvancDcHK1zuKsL39jzAg7+LYjKJfEfrsQP+lj/EQcjtKqlhVS5kzsJVfx8ZEd0xhW5G7N6bCdKNalS8mKCMaBXJpijNQ82AiyqCIDCRrre2To0/i7pTjRiL0U9f9mV3S4NJaQaokR050w/ZLySFf6F7joJT mathijs@Qrama-Mathijs' # noqa
25
26
27@base.bootstrapped
28@pytest.mark.asyncio
Adam Israelb8a82812019-03-27 14:50:11 -040029async def test_deploy_local_bundle_dir(event_loop):
Adam Israeldcdf82b2017-08-15 15:26:43 -040030 tests_dir = Path(__file__).absolute().parent.parent
31 bundle_path = tests_dir / 'bundle'
Adam Israelc3e6c2e2018-03-01 09:31:50 -050032 mini_bundle_file_path = bundle_path / 'mini-bundle.yaml'
Adam Israeldcdf82b2017-08-15 15:26:43 -040033
34 async with base.CleanModel() as model:
35 await model.deploy(str(bundle_path))
Adam Israelc3e6c2e2018-03-01 09:31:50 -050036 await model.deploy(str(mini_bundle_file_path))
Adam Israeldcdf82b2017-08-15 15:26:43 -040037
Adam Israelb8a82812019-03-27 14:50:11 -040038 wordpress = model.applications.get('wordpress')
39 mysql = model.applications.get('mysql')
40 assert wordpress and mysql
41 await block_until(lambda: (len(wordpress.units) == 1 and
42 len(mysql.units) == 1),
43 timeout=60 * 4)
44
45
46@base.bootstrapped
47@pytest.mark.asyncio
48async def test_deploy_local_bundle_file(event_loop):
49 tests_dir = Path(__file__).absolute().parent.parent
50 bundle_path = tests_dir / 'bundle'
51 mini_bundle_file_path = bundle_path / 'mini-bundle.yaml'
52
53 async with base.CleanModel() as model:
54 await model.deploy(str(mini_bundle_file_path))
55
56 dummy_sink = model.applications.get('dummy-sink')
57 dummy_subordinate = model.applications.get('dummy-subordinate')
58 assert dummy_sink and dummy_subordinate
59 await block_until(lambda: (len(dummy_sink.units) == 1 and
60 len(dummy_subordinate.units) == 1),
61 timeout=60 * 4)
62
63
64@base.bootstrapped
65@pytest.mark.asyncio
66async def test_deploy_invalid_bundle(event_loop):
67 tests_dir = Path(__file__).absolute().parent.parent
68 bundle_path = tests_dir / 'bundle' / 'invalid.yaml'
69 async with base.CleanModel() as model:
70 with pytest.raises(JujuError):
71 await model.deploy(str(bundle_path))
72
73
74@base.bootstrapped
75@pytest.mark.asyncio
76async def test_deploy_local_charm(event_loop):
77 from pathlib import Path
78 tests_dir = Path(__file__).absolute().parent.parent
79 charm_path = tests_dir / 'charm'
80
81 async with base.CleanModel() as model:
82 await model.deploy(str(charm_path))
83 assert 'charm' in model.applications
Adam Israeldcdf82b2017-08-15 15:26:43 -040084
85
86@base.bootstrapped
87@pytest.mark.asyncio
Adam Israelb0943662018-08-02 15:32:00 -040088async def test_deploy_invalid_bundle(event_loop):
89 tests_dir = Path(__file__).absolute().parent.parent
90 bundle_path = tests_dir / 'bundle' / 'invalid.yaml'
91 async with base.CleanModel() as model:
92 with pytest.raises(JujuError):
93 await model.deploy(str(bundle_path))
94
95
96@base.bootstrapped
97@pytest.mark.asyncio
Adam Israelc3e6c2e2018-03-01 09:31:50 -050098async def test_deploy_local_charm(event_loop):
99 from pathlib import Path
100 tests_dir = Path(__file__).absolute().parent.parent
101 charm_path = tests_dir / 'charm'
102
103 async with base.CleanModel() as model:
104 await model.deploy(str(charm_path))
105 assert 'charm' in model.applications
106
107
108@base.bootstrapped
109@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -0400110async def test_deploy_bundle(event_loop):
111 async with base.CleanModel() as model:
112 await model.deploy('bundle/wiki-simple')
113
114 for app in ('wiki', 'mysql'):
115 assert app in model.applications
116
117
118@base.bootstrapped
119@pytest.mark.asyncio
120async def test_deploy_channels_revs(event_loop):
121 async with base.CleanModel() as model:
122 charm = 'cs:~johnsca/libjuju-test'
123 stable = await model.deploy(charm, 'a1')
124 edge = await model.deploy(charm, 'a2', channel='edge')
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500125 rev = await model.deploy(charm + '-2', 'a3')
Adam Israeldcdf82b2017-08-15 15:26:43 -0400126
127 assert [a.charm_url for a in (stable, edge, rev)] == [
128 'cs:~johnsca/libjuju-test-1',
129 'cs:~johnsca/libjuju-test-2',
130 'cs:~johnsca/libjuju-test-2',
131 ]
132
133
134@base.bootstrapped
135@pytest.mark.asyncio
136async def test_add_machine(event_loop):
137 from juju.machine import Machine
138
139 async with base.CleanModel() as model:
140 # add a new default machine
141 machine1 = await model.add_machine()
142
143 # add a machine with constraints, disks, and series
144 machine2 = await model.add_machine(
145 constraints={
146 'mem': 256 * MB,
147 },
148 disks=[{
149 'pool': 'rootfs',
150 'size': 10 * GB,
151 'count': 1,
152 }],
153 series='xenial',
154 )
155
156 # add a lxd container to machine2
157 machine3 = await model.add_machine(
158 'lxd:{}'.format(machine2.id))
159
160 for m in (machine1, machine2, machine3):
161 assert isinstance(m, Machine)
162
163 assert len(model.machines) == 3
164
165 await machine3.destroy(force=True)
166 await machine2.destroy(force=True)
167 res = await machine1.destroy(force=True)
168
169 assert res is None
170 assert len(model.machines) == 0
171
172
173@base.bootstrapped
174@pytest.mark.asyncio
Adam Israelb0943662018-08-02 15:32:00 -0400175async def test_add_manual_machine_ssh(event_loop):
176
177 # Verify controller is localhost
178 async with base.CleanController() as controller:
179 cloud = await controller.get_cloud()
180 if cloud != "localhost":
181 pytest.skip('Skipping because test requires lxd.')
182
183 async with base.CleanModel() as model:
184 private_key_path = os.path.expanduser(
185 "~/.local/share/juju/ssh/juju_id_rsa"
186 )
187 public_key_path = os.path.expanduser(
188 "~/.local/share/juju/ssh/juju_id_rsa.pub"
189 )
190
Adam Israelb8a82812019-03-27 14:50:11 -0400191 # connect using the local unix socket
192 client = pylxd.Client()
Adam Israelb0943662018-08-02 15:32:00 -0400193
194 test_name = "test-{}-add-manual-machine-ssh".format(
195 uuid.uuid4().hex[-4:]
196 )
197
198 # create profile w/cloud-init and juju ssh key
199 public_key = ""
200 with open(public_key_path, "r") as f:
201 public_key = f.readline()
202
203 profile = client.profiles.create(
204 test_name,
Adam Israelb8a82812019-03-27 14:50:11 -0400205 config={'user.user-data': '#cloud-config\n'
206 'ssh_authorized_keys:\n'
207 '- {}'.format(public_key)},
Adam Israelb0943662018-08-02 15:32:00 -0400208 devices={
209 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
210 'eth0': {
211 'nictype': 'bridged',
212 'parent': 'lxdbr0',
213 'type': 'nic'
214 }
215 }
216 )
217
218 # create lxc machine
219 config = {
220 'name': test_name,
221 'source': {
222 'type': 'image',
223 'alias': 'xenial',
224 'mode': 'pull',
225 'protocol': 'simplestreams',
226 'server': 'https://cloud-images.ubuntu.com/releases',
227 },
228 'profiles': [test_name],
229 }
230 container = client.containers.create(config, wait=True)
231 container.start(wait=True)
232
233 def wait_for_network(container, timeout=30):
234 """Wait for eth0 to have an ipv4 address."""
235 starttime = time.time()
236 while(time.time() < starttime + timeout):
237 time.sleep(1)
238 if 'eth0' in container.state().network:
239 addresses = container.state().network['eth0']['addresses']
240 if len(addresses) > 0:
241 if addresses[0]['family'] == 'inet':
242 return addresses[0]
243 return None
244
245 host = wait_for_network(container)
Adam Israelb8a82812019-03-27 14:50:11 -0400246 assert host, 'Failed to get address for machine'
Adam Israelb0943662018-08-02 15:32:00 -0400247
248 # HACK: We need to give sshd a chance to bind to the interface,
249 # and pylxd's container.execute seems to be broken and fails and/or
250 # hangs trying to properly check if the service is up.
251 time.sleep(5)
252
Adam Israelb8a82812019-03-27 14:50:11 -0400253 for attempt in range(1, 4):
254 try:
255 # add a new manual machine
256 machine1 = await model.add_machine(spec='ssh:{}@{}:{}'.format(
257 "ubuntu",
258 host['address'],
259 private_key_path,
260 ))
261 except paramiko.ssh_exception.NoValidConnectionsError:
262 if attempt == 3:
263 raise
264 # retry the ssh connection a few times if it fails
265 time.sleep(attempt * 5)
266 else:
267 break
Adam Israelb0943662018-08-02 15:32:00 -0400268
269 assert len(model.machines) == 1
270
271 res = await machine1.destroy(force=True)
272
273 assert res is None
274 assert len(model.machines) == 0
275
276 container.stop(wait=True)
277 container.delete(wait=True)
278
279 profile.delete()
280
281
282@base.bootstrapped
283@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -0400284async def test_relate(event_loop):
285 from juju.relation import Relation
286
287 async with base.CleanModel() as model:
288 await model.deploy(
289 'ubuntu',
290 application_name='ubuntu',
291 series='trusty',
292 channel='stable',
293 )
294 await model.deploy(
295 'nrpe',
296 application_name='nrpe',
297 series='trusty',
298 channel='stable',
299 # subordinates must be deployed without units
300 num_units=0,
301 )
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500302
303 relation_added = asyncio.Event()
304 timeout = asyncio.Event()
305
306 class TestObserver(ModelObserver):
307 async def on_relation_add(self, delta, old, new, model):
308 if set(new.key.split()) == {'nrpe:general-info',
309 'ubuntu:juju-info'}:
310 relation_added.set()
311 event_loop.call_later(2, timeout.set)
312
313 model.add_observer(TestObserver())
314
315 real_app_facade = ApplicationFacade.from_connection(model.connection())
316 mock_app_facade = mock.MagicMock()
317
318 async def mock_AddRelation(*args):
319 # force response delay from AddRelation to test race condition
320 # (see https://github.com/juju/python-libjuju/issues/191)
321 result = await real_app_facade.AddRelation(*args)
322 await relation_added.wait()
323 return result
324
325 mock_app_facade.AddRelation = mock_AddRelation
326
327 with mock.patch.object(ApplicationFacade, 'from_connection',
328 return_value=mock_app_facade):
329 my_relation = await run_with_interrupt(model.add_relation(
330 'ubuntu',
331 'nrpe',
Adam Israelb8a82812019-03-27 14:50:11 -0400332 ), timeout, loop=event_loop)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400333
334 assert isinstance(my_relation, Relation)
335
336
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500337async def _deploy_in_loop(new_loop, model_name, jujudata):
338 new_model = Model(new_loop, jujudata=jujudata)
339 await new_model.connect(model_name)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400340 try:
341 await new_model.deploy('cs:xenial/ubuntu')
342 assert 'ubuntu' in new_model.applications
343 finally:
344 await new_model.disconnect()
345
346
347@base.bootstrapped
348@pytest.mark.asyncio
Adam Israeldcdf82b2017-08-15 15:26:43 -0400349async def test_explicit_loop_threaded(event_loop):
350 async with base.CleanModel() as model:
351 model_name = model.info.name
352 new_loop = asyncio.new_event_loop()
353 with ThreadPoolExecutor(1) as executor:
354 f = executor.submit(
355 new_loop.run_until_complete,
Adam Israelb8a82812019-03-27 14:50:11 -0400356 _deploy_in_loop(new_loop,
357 model_name,
358 model._connector.jujudata))
Adam Israeldcdf82b2017-08-15 15:26:43 -0400359 f.result()
360 await model._wait_for_new('application', 'ubuntu')
361 assert 'ubuntu' in model.applications
362
363
364@base.bootstrapped
365@pytest.mark.asyncio
366async def test_store_resources_charm(event_loop):
367 async with base.CleanModel() as model:
368 ghost = await model.deploy('cs:ghost-19')
369 assert 'ghost' in model.applications
370 terminal_statuses = ('active', 'error', 'blocked')
371 await model.block_until(
372 lambda: (
373 len(ghost.units) > 0 and
374 ghost.units[0].workload_status in terminal_statuses)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500375 )
Adam Israeldcdf82b2017-08-15 15:26:43 -0400376 # ghost will go in to blocked (or error, for older
377 # charm revs) if the resource is missing
378 assert ghost.units[0].workload_status == 'active'
379
380
381@base.bootstrapped
382@pytest.mark.asyncio
383async def test_store_resources_bundle(event_loop):
384 async with base.CleanModel() as model:
385 bundle = str(Path(__file__).parent / 'bundle')
386 await model.deploy(bundle)
387 assert 'ghost' in model.applications
388 ghost = model.applications['ghost']
389 terminal_statuses = ('active', 'error', 'blocked')
390 await model.block_until(
391 lambda: (
392 len(ghost.units) > 0 and
393 ghost.units[0].workload_status in terminal_statuses)
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500394 )
Adam Israeldcdf82b2017-08-15 15:26:43 -0400395 # ghost will go in to blocked (or error, for older
396 # charm revs) if the resource is missing
397 assert ghost.units[0].workload_status == 'active'
Adam Israelb8a82812019-03-27 14:50:11 -0400398 resources = await ghost.get_resources()
399 assert resources['ghost-stable'].revision >= 12
400
401
402@base.bootstrapped
403@pytest.mark.asyncio
404async def test_store_resources_bundle_revs(event_loop):
405 async with base.CleanModel() as model:
406 bundle = str(Path(__file__).parent / 'bundle/bundle-resource-rev.yaml')
407 await model.deploy(bundle)
408 assert 'ghost' in model.applications
409 ghost = model.applications['ghost']
410 terminal_statuses = ('active', 'error', 'blocked')
411 await model.block_until(
412 lambda: (
413 len(ghost.units) > 0 and
414 ghost.units[0].workload_status in terminal_statuses)
415 )
416 # ghost will go in to blocked (or error, for older
417 # charm revs) if the resource is missing
418 assert ghost.units[0].workload_status == 'active'
419 resources = await ghost.get_resources()
420 assert resources['ghost-stable'].revision == 11
Adam Israeldcdf82b2017-08-15 15:26:43 -0400421
422
423@base.bootstrapped
424@pytest.mark.asyncio
425async def test_ssh_key(event_loop):
426 async with base.CleanModel() as model:
427 await model.add_ssh_key('admin', SSH_KEY)
428 result = await model.get_ssh_key(True)
429 result = result.serialize()['results'][0].serialize()['result']
430 assert SSH_KEY in result
431 await model.remove_ssh_key('admin', SSH_KEY)
432 result = await model.get_ssh_key(True)
433 result = result.serialize()['results'][0].serialize()['result']
434 assert result is None
435
436
437@base.bootstrapped
438@pytest.mark.asyncio
439async def test_get_machines(event_loop):
440 async with base.CleanModel() as model:
441 result = await model.get_machines()
442 assert isinstance(result, list)
443
444
445@base.bootstrapped
446@pytest.mark.asyncio
447async def test_watcher_reconnect(event_loop):
448 async with base.CleanModel() as model:
Adam Israelc3e6c2e2018-03-01 09:31:50 -0500449 await model.connection().ws.close()
450 await block_until(model.is_connected, timeout=3)
Adam Israeldcdf82b2017-08-15 15:26:43 -0400451
452
453@base.bootstrapped
454@pytest.mark.asyncio
455async def test_config(event_loop):
456 async with base.CleanModel() as model:
457 await model.set_config({
458 'extra-info': 'booyah',
459 'test-mode': ConfigValue(value=True),
460 })
461 result = await model.get_config()
462 assert 'extra-info' in result
463 assert result['extra-info'].source == 'model'
464 assert result['extra-info'].value == 'booyah'
465
Adam Israelb8a82812019-03-27 14:50:11 -0400466
Adam Israelb0943662018-08-02 15:32:00 -0400467@base.bootstrapped
468@pytest.mark.asyncio
469async def test_set_constraints(event_loop):
470 async with base.CleanModel() as model:
471 await model.set_constraints({'cpu-power': 1})
472 cons = await model.get_constraints()
473 assert cons['cpu_power'] == 1
474
Adam Israeldcdf82b2017-08-15 15:26:43 -0400475# @base.bootstrapped
476# @pytest.mark.asyncio
477# async def test_grant(event_loop)
478# async with base.CleanController() as controller:
479# await controller.add_user('test-model-grant')
480# await controller.grant('test-model-grant', 'superuser')
481# async with base.CleanModel() as model:
482# await model.grant('test-model-grant', 'admin')
483# assert model.get_user('test-model-grant')['access'] == 'admin'
484# await model.grant('test-model-grant', 'login')
485# assert model.get_user('test-model-grant')['access'] == 'login'