Squashed 'modules/libjuju/' changes from c50c361..c127833
[osm/N2VC.git] / 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
33 async with base.CleanModel() as model:
34 await model.deploy(str(bundle_path))
35
36 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
46 async 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
64 async 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
74 async 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
82
83
84 @base.bootstrapped
85 @pytest.mark.asyncio
86 async 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
96 async 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')
101 rev = await model.deploy(charm + '-2', 'a3')
102
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
112 async 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
151 async 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
260 async 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 )
278
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)
309
310 assert isinstance(my_relation, Relation)
311
312
313 async def _deploy_in_loop(new_loop, model_name, jujudata):
314 new_model = Model(new_loop, jujudata=jujudata)
315 await new_model.connect(model_name)
316 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
325 async 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,
332 _deploy_in_loop(new_loop,
333 model_name,
334 model._connector.jujudata))
335 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
342 async 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)
351 )
352 # 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
359 async 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)
370 )
371 # 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'
374 resources = await ghost.get_resources()
375 assert resources['ghost-stable'].revision >= 12
376
377
378 @base.bootstrapped
379 @pytest.mark.asyncio
380 async 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
397
398
399 @base.bootstrapped
400 @pytest.mark.asyncio
401 async 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
415 async 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
423 async def test_watcher_reconnect(event_loop):
424 async with base.CleanModel() as model:
425 await model.connection().ws.close()
426 await block_until(model.is_connected, timeout=3)
427
428
429 @base.bootstrapped
430 @pytest.mark.asyncio
431 async 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
442
443 @base.bootstrapped
444 @pytest.mark.asyncio
445 async 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
451 # @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'