3 from concurrent
.futures
import ThreadPoolExecutor
4 from pathlib
import Path
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
24 SSH_KEY
= 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQCsYMJGNGG74HAJha3n2CFmWYsOOaORnJK6VqNy86pj0MIpvRXBzFzVy09uPQ66GOQhTEoJHEqE77VMui7+62AcMXT+GG7cFHcnU8XVQsGM6UirCcNyWNysfiEMoAdZScJf/GvoY87tMEszhZIUV37z8PUBx6twIqMdr31W1J0IaPa+sV6FEDadeLaNTvancDcHK1zuKsL39jzAg7+LYjKJfEfrsQP+lj/EQcjtKqlhVS5kzsJVfx8ZEd0xhW5G7N6bCdKNalS8mKCMaBXJpijNQ82AiyqCIDCRrre2To0/i7pTjRiL0U9f9mV3S4NJaQaokR050w/ZLySFf6F7joJT mathijs@Qrama-Mathijs' # noqa
29 async def test_deploy_local_bundle_dir(event_loop
):
30 tests_dir
= Path(__file__
).absolute().parent
.parent
31 bundle_path
= tests_dir
/ 'bundle'
33 async with base
.CleanModel() as model
:
34 await model
.deploy(str(bundle_path
))
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),
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'
51 async with base
.CleanModel() as model
:
52 await model
.deploy(str(mini_bundle_file_path
))
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),
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
))
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'
79 async with base
.CleanModel() as model
:
80 await model
.deploy(str(charm_path
))
81 assert 'charm' in model
.applications
86 async def test_deploy_bundle(event_loop
):
87 async with base
.CleanModel() as model
:
88 await model
.deploy('bundle/wiki-simple')
90 for app
in ('wiki', 'mysql'):
91 assert app
in model
.applications
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')
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',
112 async def test_add_machine(event_loop
):
113 from juju
.machine
import Machine
115 async with base
.CleanModel() as model
:
116 # add a new default machine
117 machine1
= await model
.add_machine()
119 # add a machine with constraints, disks, and series
120 machine2
= await model
.add_machine(
132 # add a lxd container to machine2
133 machine3
= await model
.add_machine(
134 'lxd:{}'.format(machine2
.id))
136 for m
in (machine1
, machine2
, machine3
):
137 assert isinstance(m
, Machine
)
139 assert len(model
.machines
) == 3
141 await machine3
.destroy(force
=True)
142 await machine2
.destroy(force
=True)
143 res
= await machine1
.destroy(force
=True)
146 assert len(model
.machines
) == 0
151 async def test_add_manual_machine_ssh(event_loop
):
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.')
159 async with base
.CleanModel() as model
:
160 private_key_path
= os
.path
.expanduser(
161 "~/.local/share/juju/ssh/juju_id_rsa"
163 public_key_path
= os
.path
.expanduser(
164 "~/.local/share/juju/ssh/juju_id_rsa.pub"
167 # connect using the local unix socket
168 client
= pylxd
.Client()
170 test_name
= "test-{}-add-manual-machine-ssh".format(
171 uuid
.uuid4().hex[-4:]
174 # create profile w/cloud-init and juju ssh key
176 with
open(public_key_path
, "r") as f
:
177 public_key
= f
.readline()
179 profile
= client
.profiles
.create(
181 config
={'user.user-data': '#cloud-config\n'
182 'ssh_authorized_keys:\n'
183 '- {}'.format(public_key
)},
185 'root': {'path': '/', 'pool': 'default', 'type': 'disk'},
187 'nictype': 'bridged',
201 'protocol': 'simplestreams',
202 'server': 'https://cloud-images.ubuntu.com/releases',
204 'profiles': [test_name
],
206 container
= client
.containers
.create(config
, wait
=True)
207 container
.start(wait
=True)
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
):
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':
221 host
= wait_for_network(container
)
222 assert host
, 'Failed to get address for machine'
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.
229 for attempt
in range(1, 4):
231 # add a new manual machine
232 machine1
= await model
.add_machine(spec
='ssh:{}@{}:{}'.format(
237 except paramiko
.ssh_exception
.NoValidConnectionsError
:
240 # retry the ssh connection a few times if it fails
241 time
.sleep(attempt
* 5)
245 assert len(model
.machines
) == 1
247 res
= await machine1
.destroy(force
=True)
250 assert len(model
.machines
) == 0
252 container
.stop(wait
=True)
253 container
.delete(wait
=True)
260 async def test_relate(event_loop
):
261 from juju
.relation
import Relation
263 async with base
.CleanModel() as model
:
266 application_name
='ubuntu',
272 application_name
='nrpe',
275 # subordinates must be deployed without units
279 relation_added
= asyncio
.Event()
280 timeout
= asyncio
.Event()
282 class TestObserver(ModelObserver
):
283 async def on_relation_add(self
, delta
, old
, new
, model
):
284 if set(new
.key
.split()) == {'nrpe:general-info',
287 event_loop
.call_later(2, timeout
.set)
289 model
.add_observer(TestObserver())
291 real_app_facade
= ApplicationFacade
.from_connection(model
.connection())
292 mock_app_facade
= mock
.MagicMock()
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()
301 mock_app_facade
.AddRelation
= mock_AddRelation
303 with mock
.patch
.object(ApplicationFacade
, 'from_connection',
304 return_value
=mock_app_facade
):
305 my_relation
= await run_with_interrupt(model
.add_relation(
308 ), timeout
, loop
=event_loop
)
310 assert isinstance(my_relation
, Relation
)
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
)
317 await new_model
.deploy('cs:xenial/ubuntu')
318 assert 'ubuntu' in new_model
.applications
320 await new_model
.disconnect()
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
:
331 new_loop
.run_until_complete
,
332 _deploy_in_loop(new_loop
,
334 model
._connector
.jujudata
))
336 await model
._wait
_for
_new
('application', 'ubuntu')
337 assert 'ubuntu' in model
.applications
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(
349 len(ghost
.units
) > 0 and
350 ghost
.units
[0].workload_status
in terminal_statuses
)
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'
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(
368 len(ghost
.units
) > 0 and
369 ghost
.units
[0].workload_status
in terminal_statuses
)
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
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(
389 len(ghost
.units
) > 0 and
390 ghost
.units
[0].workload_status
in terminal_statuses
)
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
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
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)
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)
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),
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'
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
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'