Native charm support
[osm/N2VC.git] / modules / libjuju / tests / integration / test_model.py
1 import asyncio
2 import mock
3 from concurrent.futures import ThreadPoolExecutor
4 from pathlib import Path
5 import paramiko
6
7 from juju.client.client import ConfigValue, ApplicationFacade
8 from juju.model import Model, ModelObserver
9 from juju.utils import block_until, run_with_interrupt
10 from juju.errors import JujuError
11
12 import os
13 import pylxd
14 import time
15 import uuid
16
17 import pytest
18
19 from .. import base
20
21
22 MB = 1
23 GB = 1024
24 SSH_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
29 async def test_deploy_local_bundle_dir(event_loop):
30 tests_dir = Path(__file__).absolute().parent.parent
31 bundle_path = tests_dir / 'bundle'
32 mini_bundle_file_path = bundle_path / 'mini-bundle.yaml'
33
34 async with base.CleanModel() as model:
35 await model.deploy(str(bundle_path))
36 await model.deploy(str(mini_bundle_file_path))
37
38 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
48 async 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
66 async 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
76 async 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
84
85
86 @base.bootstrapped
87 @pytest.mark.asyncio
88 async 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
98 async 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
110 async 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
120 async 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')
125 rev = await model.deploy(charm + '-2', 'a3')
126
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
136 async 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
175 async 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
191 # connect using the local unix socket
192 client = pylxd.Client()
193
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,
205 config={'user.user-data': '#cloud-config\n'
206 'ssh_authorized_keys:\n'
207 '- {}'.format(public_key)},
208 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)
246 assert host, 'Failed to get address for machine'
247
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
253 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
268
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
284 async 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 )
302
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',
332 ), timeout, loop=event_loop)
333
334 assert isinstance(my_relation, Relation)
335
336
337 async def _deploy_in_loop(new_loop, model_name, jujudata):
338 new_model = Model(new_loop, jujudata=jujudata)
339 await new_model.connect(model_name)
340 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
349 async 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,
356 _deploy_in_loop(new_loop,
357 model_name,
358 model._connector.jujudata))
359 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
366 async 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)
375 )
376 # 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
383 async 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)
394 )
395 # 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'
398 resources = await ghost.get_resources()
399 assert resources['ghost-stable'].revision >= 12
400
401
402 @base.bootstrapped
403 @pytest.mark.asyncio
404 async 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
421
422
423 @base.bootstrapped
424 @pytest.mark.asyncio
425 async 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
439 async 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
447 async def test_watcher_reconnect(event_loop):
448 async with base.CleanModel() as model:
449 await model.connection().ws.close()
450 await block_until(model.is_connected, timeout=3)
451
452
453 @base.bootstrapped
454 @pytest.mark.asyncio
455 async 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
466
467 @base.bootstrapped
468 @pytest.mark.asyncio
469 async 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
475 # @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'