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