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