Merge lp:~frankban/juju-quickstart/new-machine-info into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 62
Proposed branch: lp:~frankban/juju-quickstart/new-machine-info
Merge into: lp:juju-quickstart
Diff against target: 943 lines (+504/-140)
6 files modified
Makefile (+2/-1)
quickstart/app.py (+19/-7)
quickstart/models/envs.py (+3/-3)
quickstart/tests/test_app.py (+209/-109)
quickstart/tests/test_watchers.py (+179/-11)
quickstart/watchers.py (+92/-9)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/new-machine-info
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+214317@code.launchpad.net

Description of the change

Support MachineInfo addresses.

juju-core 1.18 introduces a change in the mega-watcher:
the MachineInfo includes the public/private addresses
for each machine/container in the environment.
This will be the preferred way to retrieve entity
addresses in future versions of juju-core, which
might also discard the public address field in
the UnitInfo.

This branch updates quickstart so that it can work
in both scenarios: for backward compatibility, the address
is retrieved trying to parse both the unit and the machine
info, without assuming the corresponding fields to be
always included.

This required some testing and documentation efforts,
resulting in a diff longer than usual: sorry about that.

Tests: `make check`.

QA:
Use juju 1.16 (current stable).
In the steps below the command to run is:
`.venv/bin/python juju-quickstart -e {ENV_NAME}`.

1) Bootstrap a local environment with quickstart, ensure
   the quickstart process completes correctly, the juju-gui
   address is retrieved, the GUI is opened. Also ensure
   the user messages showed on stdout make sense.
2) Execute quickstart again, with the local environment already
   bootstrapped. Ensure the process completes correctly,
   and user messages are sane.
3) Destroy the local environment.
4) Bootstrap an ec2 environment with quickstart, ensure
   the quickstart process completes correctly, the juju-gui
   address is retrieved, the GUI is opened. Also ensure
   the user messages showed on stdout make sense.
5) Execute quickstart again, with the ec2 environment already
   bootstrapped. Ensure the process completes correctly,
   and user messages are sane.
6) Destroy the ec2 environment.

Use juju 1.18. This must be compiled from the juju-core 1.18 branch,
which can be found in `lp:juju-core/1.18`.

7) Edit the quickstart/settings.py file included in this branch:
   set `JUJU_CMD` to point to the juju 1.18 path.
8) Follow steps 1) to 6) again, in order to check that
   quickstart works well also with Juju 1.18.

Done, thank you!

https://codereview.appspot.com/84630043/

To post a comment you must log in.
Revision history for this message
Francesco Banconi (frankban) wrote :

Reviewers: mp+214317_code.launchpad.net,

Message:
Please take a look.

Description:
Support MachineInfo addresses.

juju-core 1.18 introduces a change in the mega-watcher:
the MachineInfo includes the public/private addresses
for each machine/container in the environment.
This will be the preferred way to retrieve entity
addresses in future versions of juju-core, which
might also discard the public address field in
the UnitInfo.

This branch updates quickstart so that it can work
in both scenarios: for backward compatibility, the address
is retrieved trying to parse both the unit and the machine
info, without assuming the corresponding fields to be
always included.

This required some testing and documentation efforts,
resulting in a diff longer than usual: sorry about that.

Tests: `make check`.

QA:
Use juju 1.16 (current stable).
In the steps below the command to run is:
`.venv/bin/python juju-quickstart -e {ENV_NAME}`.

1) Bootstrap a local environment with quickstart, ensure
    the quickstart process completes correctly, the juju-gui
    address is retrieved, the GUI is opened. Also ensure
    the user messages showed on stdout make sense.
2) Execute quickstart again, with the local environment already
    bootstrapped. Ensure the process completes correctly,
    and user messages are sane.
3) Destroy the local environment.
4) Bootstrap an ec2 environment with quickstart, ensure
    the quickstart process completes correctly, the juju-gui
    address is retrieved, the GUI is opened. Also ensure
    the user messages showed on stdout make sense.
5) Execute quickstart again, with the ec2 environment already
    bootstrapped. Ensure the process completes correctly,
    and user messages are sane.
6) Destroy the ec2 environment.

Use juju 1.18. This must be compiled from the juju-core 1.18 branch,
which can be found in `lp:juju-core/1.18`.

7) Edit the quickstart/settings.py file included in this branch:
    set `JUJU_CMD` to point to the juju 1.18 path.
8) Follow steps 1) to 6) again, in order to check that
    quickstart works well also with Juju 1.18.

Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/new-machine-info/+merge/214317

(do not edit description out of merge proposal)

Please review this at https://codereview.appspot.com/84630043/

Affected files (+500, -139 lines):
   A [revision details]
   M quickstart/app.py
   M quickstart/models/envs.py
   M quickstart/tests/test_app.py
   M quickstart/tests/test_watchers.py
   M quickstart/watchers.py

Revision history for this message
Brad Crittenden (bac) wrote :

LGTM. Will QA now.

https://codereview.appspot.com/84630043/diff/1/quickstart/models/envs.py
File quickstart/models/envs.py (right):

https://codereview.appspot.com/84630043/diff/1/quickstart/models/envs.py#newcode444
quickstart/models/envs.py:444: 'The ec2 provider enables you to run Juju
on the EC2 cloud. '
This change looks familiar...

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py
File quickstart/tests/test_watchers.py (right):

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py#newcode85
quickstart/tests/test_watchers.py:85: # None is returned if an LXC
public address is not available.
Two spaces: LXC public

(I know you hate those!)

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py#newcode114
quickstart/tests/test_watchers.py:114: # The public address of an LXC
instance is proprely returned.
typo: properly

https://codereview.appspot.com/84630043/

Revision history for this message
Brad Crittenden (bac) wrote :
66. By Francesco Banconi

Add debug to the Makefile.

67. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :

Please take a look.

https://codereview.appspot.com/84630043/diff/1/quickstart/models/envs.py
File quickstart/models/envs.py (right):

https://codereview.appspot.com/84630043/diff/1/quickstart/models/envs.py#newcode444
quickstart/models/envs.py:444: 'The ec2 provider enables you to run Juju
on the EC2 cloud. '
On 2014/04/04 18:24:04, bac wrote:
> This change looks familiar...

Yeah, I really wanted to have these typos fixed in trusty.

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py
File quickstart/tests/test_watchers.py (right):

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py#newcode85
quickstart/tests/test_watchers.py:85: # None is returned if an LXC
public address is not available.
On 2014/04/04 18:24:04, bac wrote:
> Two spaces: LXC public

> (I know you hate those!)

Indeed! Thank you for spotting this.

https://codereview.appspot.com/84630043/diff/1/quickstart/tests/test_watchers.py#newcode114
quickstart/tests/test_watchers.py:114: # The public address of an LXC
instance is proprely returned.
On 2014/04/04 18:24:04, bac wrote:
> typo: properly

Done.

https://codereview.appspot.com/84630043/

Revision history for this message
Richard Harding (rharding) wrote :

LGTM, couple of questions/comments. I think I follow the first one now.
If only you could order files in review to line up better.

https://codereview.appspot.com/84630043/diff/20001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/84630043/diff/20001/quickstart/app.py#newcode420
quickstart/app.py:420: collected_machine_changes = []
Can we use something like a hash keyed by machine_id (or something else
since it might not have an id per below) and then just update the
changes/state as it comes in to not need to sort/reverse/etc everything.
Just always keep the latest info for the key? I'm guessing not, but
curious as to the process.

https://codereview.appspot.com/84630043/diff/20001/quickstart/tests/test_watchers.py
File quickstart/tests/test_watchers.py (right):

https://codereview.appspot.com/84630043/diff/20001/quickstart/tests/test_watchers.py#newcode52
quickstart/tests/test_watchers.py:52: lxc_addresses = [
these can be kvm as well correct? Should these be tested as part of this
or is there no difference other than the three chars?

https://codereview.appspot.com/84630043/

68. By Francesco Banconi

Changes as per review.

Revision history for this message
Francesco Banconi (frankban) wrote :
Download full text (3.5 KiB)

*** Submitted:

Support MachineInfo addresses.

juju-core 1.18 introduces a change in the mega-watcher:
the MachineInfo includes the public/private addresses
for each machine/container in the environment.
This will be the preferred way to retrieve entity
addresses in future versions of juju-core, which
might also discard the public address field in
the UnitInfo.

This branch updates quickstart so that it can work
in both scenarios: for backward compatibility, the address
is retrieved trying to parse both the unit and the machine
info, without assuming the corresponding fields to be
always included.

This required some testing and documentation efforts,
resulting in a diff longer than usual: sorry about that.

Tests: `make check`.

QA:
Use juju 1.16 (current stable).
In the steps below the command to run is:
`.venv/bin/python juju-quickstart -e {ENV_NAME}`.

1) Bootstrap a local environment with quickstart, ensure
    the quickstart process completes correctly, the juju-gui
    address is retrieved, the GUI is opened. Also ensure
    the user messages showed on stdout make sense.
2) Execute quickstart again, with the local environment already
    bootstrapped. Ensure the process completes correctly,
    and user messages are sane.
3) Destroy the local environment.
4) Bootstrap an ec2 environment with quickstart, ensure
    the quickstart process completes correctly, the juju-gui
    address is retrieved, the GUI is opened. Also ensure
    the user messages showed on stdout make sense.
5) Execute quickstart again, with the ec2 environment already
    bootstrapped. Ensure the process completes correctly,
    and user messages are sane.
6) Destroy the ec2 environment.

Use juju 1.18. This must be compiled from the juju-core 1.18 branch,
which can be found in `lp:juju-core/1.18`.

7) Edit the quickstart/settings.py file included in this branch:
    set `JUJU_CMD` to point to the juju 1.18 path.
8) Follow steps 1) to 6) again, in order to check that
    quickstart works well also with Juju 1.18.

Done, thank you!

R=bac, rharding
CC=
https://codereview.appspot.com/84630043

https://codereview.appspot.com/84630043/diff/20001/quickstart/app.py
File quickstart/app.py (right):

https://codereview.appspot.com/84630043/diff/20001/quickstart/app.py#newcode420
quickstart/app.py:420: collected_machine_changes = []
On 2014/04/07 12:57:51, rharding wrote:
> Can we use something like a hash keyed by machine_id (or something
else since it
> might not have an id per below) and then just update the changes/state
as it
> comes in to not need to sort/reverse/etc everything. Just always keep
the latest
> info for the key? I'm guessing not, but curious as to the process.

That can be an option, but I preferred to just collect changes rather
than parse them here and update a mutable data structure.
Since the current approach has already been QA'd, I'd be inclined to
preserve this code, at least for this branch.

https://codereview.appspot.com/84630043/diff/20001/quickstart/tests/test_watchers.py
File quickstart/tests/test_watchers.py (right):

https://codereview.appspot.com/84630043/diff/20001/quickstart/tests/test_watchers.py#newcode52
quickstart/tests/test_watcher...

Read more...

Revision history for this message
Francesco Banconi (frankban) wrote :

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
=== modified file 'Makefile'
--- Makefile 2014-01-31 21:00:06 +0000
+++ Makefile 2014-04-07 13:24:46 +0000
@@ -85,4 +85,5 @@
85 --with-coverage --cover-package=quickstart quickstart85 --with-coverage --cover-package=quickstart quickstart
86 @rm .coverage86 @rm .coverage
8787
88.PHONY: all clean check help install lint release run setup source sysdeps test88.PHONY: all clean check debug help install lint release run setup source \
89 sysdeps test
8990
=== modified file 'quickstart/app.py'
--- quickstart/app.py 2014-03-27 10:38:01 +0000
+++ quickstart/app.py 2014-04-07 13:24:46 +0000
@@ -417,6 +417,7 @@
417 either the service unit or the machine is removed or in error.417 either the service unit or the machine is removed or in error.
418 """418 """
419 address = unit_status = machine_id = machine_status = ''419 address = unit_status = machine_id = machine_status = ''
420 collected_machine_changes = []
420 watcher = env.watch_changes(watchers.unit_machine_changes)421 watcher = env.watch_changes(watchers.unit_machine_changes)
421 while True:422 while True:
422 try:423 try:
@@ -424,7 +425,7 @@
424 except jujuclient.EnvError as err:425 except jujuclient.EnvError as err:
425 raise ProgramExit(426 raise ProgramExit(
426 'bad API server response: {}'.format(err.message))427 'bad API server response: {}'.format(err.message))
427 # Process unit changes:428 # Process unit changes.
428 for action, data in unit_changes:429 for action, data in unit_changes:
429 if data['Name'] == unit_name:430 if data['Name'] == unit_name:
430 try:431 try:
@@ -440,17 +441,28 @@
440 # unit. For this reason, we can exit the for loop here.441 # unit. For this reason, we can exit the for loop here.
441 break442 break
442 if not machine_id:443 if not machine_id:
443 # No need to process machine changes: we still don't know what444 # No need to process machine changes: we don't know what machine
444 # machine the unit belongs to.445 # the unit belongs to. However, machine changes are collected so
446 # that they can be parsed later.
447 collected_machine_changes.extend(machine_changes)
445 continue448 continue
446 # Process machine changes.449 # Process machine changes. Since relevant info can also be found
447 for action, data in machine_changes:450 # in previously collected changes, add those to the current changes,
451 # in reverse order so that more complete info comes first.
452 all_machine_changes = machine_changes + list(
453 reversed(collected_machine_changes))
454 # At this point we can discard collected changes.
455 collected_machine_changes = []
456 for action, data in all_machine_changes:
448 if data['Id'] == machine_id:457 if data['Id'] == machine_id:
449 try:458 try:
450 machine_status = watchers.parse_machine_change(459 machine_status, address = watchers.parse_machine_change(
451 action, data, machine_status)460 action, data, machine_status, address)
452 except ValueError as err:461 except ValueError as err:
453 raise ProgramExit(bytes(err))462 raise ProgramExit(bytes(err))
463 if address and unit_status == 'started':
464 # We can exit this loop.
465 return address
454 # The mega-watcher contains a single change for each specific466 # The mega-watcher contains a single change for each specific
455 # machine. For this reason, we can exit the for loop here.467 # machine. For this reason, we can exit the for loop here.
456 break468 break
457469
=== modified file 'quickstart/models/envs.py'
--- quickstart/models/envs.py 2014-01-28 20:35:42 +0000
+++ quickstart/models/envs.py 2014-04-07 13:24:46 +0000
@@ -441,7 +441,7 @@
441 env_type_db['ec2'] = {441 env_type_db['ec2'] = {
442 'label': 'Amazon EC2',442 'label': 'Amazon EC2',
443 'description': (443 'description': (
444 'The ec2 provider enable you to run Juju on the EC2 cloud. '444 'The ec2 provider enables you to run Juju on the EC2 cloud. '
445 'This process requires you to have an Amazon Web Services (AWS) '445 'This process requires you to have an Amazon Web Services (AWS) '
446 'account. If you have not signed up for one yet, it can obtained '446 'account. If you have not signed up for one yet, it can obtained '
447 'at http://aws.amazon.com. '447 'at http://aws.amazon.com. '
@@ -481,7 +481,7 @@
481 env_type_db['openstack'] = {481 env_type_db['openstack'] = {
482 'label': 'OpenStack (or HP Public Cloud)',482 'label': 'OpenStack (or HP Public Cloud)',
483 'description': (483 'description': (
484 'The openstack provider enable you to run Juju on OpenStack '484 'The openstack provider enables you to run Juju on OpenStack '
485 'private and public clouds. Use this also if you want to '485 'private and public clouds. Use this also if you want to '
486 'set up Juju on HP Public Cloud. See '486 'set up Juju on HP Public Cloud. See '
487 'https://juju.ubuntu.com/docs/config-openstack.html and '487 'https://juju.ubuntu.com/docs/config-openstack.html and '
@@ -546,7 +546,7 @@
546 env_type_db['azure'] = {546 env_type_db['azure'] = {
547 'label': 'Windows Azure',547 'label': 'Windows Azure',
548 'description': (548 'description': (
549 'The azure provider enable you to run Juju on Windows Azure. '549 'The azure provider enables you to run Juju on Windows Azure. '
550 'It requires you to have an Windows Azure account. If you have '550 'It requires you to have an Windows Azure account. If you have '
551 'not signed up for one yet, it can obtained at '551 'not signed up for one yet, it can obtained at '
552 'http://www.windowsazure.com/. See '552 'http://www.windowsazure.com/. See '
553553
=== modified file 'quickstart/tests/test_app.py'
--- quickstart/tests/test_app.py 2014-03-27 11:00:36 +0000
+++ quickstart/tests/test_app.py 2014-04-07 13:24:46 +0000
@@ -1037,43 +1037,11 @@
1037 unittest.TestCase):1037 unittest.TestCase):
10381038
1039 address = 'unit.example.com'1039 address = 'unit.example.com'
1040 change_machine_pending = ('change', {
1041 'Id': '0',
1042 'Status': 'pending',
1043 })
1044 change_machine_started = ('change', {
1045 'Id': '0',
1046 'Status': 'started',
1047 })
1048 change_unit_pending = ('change', {
1049 'MachineId': '0',
1050 'Name': 'django/42',
1051 'PublicAddress': '',
1052 'Status': 'pending',
1053 })
1054 change_unit_reachable = ('change', {
1055 'MachineId': '0',
1056 'Name': 'django/42',
1057 'PublicAddress': address,
1058 'Status': 'pending',
1059 })
1060 change_unit_installed = ('change', {
1061 'MachineId': '0',
1062 'Name': 'django/42',
1063 'PublicAddress': address,
1064 'Status': 'installed',
1065 })
1066 change_unit_started = ('change', {
1067 'MachineId': '0',
1068 'Name': 'django/42',
1069 'PublicAddress': address,
1070 'Status': 'started',
1071 })
1072 machine_pending_call = mock.call('machine 0 provisioning is pending')1040 machine_pending_call = mock.call('machine 0 provisioning is pending')
1041 unit_placed_machine_call = mock.call('unit placed on unit.example.com')
1073 machine_started_call = mock.call('machine 0 is started')1042 machine_started_call = mock.call('machine 0 is started')
1074 unit_pending_call = mock.call('django/42 deployment is pending')1043 unit_pending_call = mock.call('django/42 deployment is pending')
1075 unit_reachable_call = mock.call(1044 unit_placed_unit_call = mock.call('django/42 placed on {}'.format(address))
1076 'setting up django/42 on {}'.format(address))
1077 unit_installed_call = mock.call('django/42 is installed')1045 unit_installed_call = mock.call('django/42 is installed')
1078 unit_started_call = mock.call('django/42 is ready on machine 0')1046 unit_started_call = mock.call('django/42 is ready on machine 0')
10791047
@@ -1087,6 +1055,33 @@
1087 env.watch_changes().next.side_effect = changes1055 env.watch_changes().next.side_effect = changes
1088 return env1056 return env
10891057
1058 def make_machine_change(self, status, name='0', address=None):
1059 """Create and return a machine change.
1060
1061 If the address argument is None, the change does not include the
1062 corresponding address field.
1063 """
1064 data = {'Id': name, 'Status': status}
1065 if address is not None:
1066 data['Addresses'] = [{
1067 'NetworkName': '',
1068 'NetworkScope': 'public',
1069 'Type': 'hostname',
1070 'Value': address,
1071 }]
1072 return 'change', data
1073
1074 def make_unit_change(self, status, name='django/42', address=None):
1075 """Create and return a unit change.
1076
1077 If the address argument is None, the change does not include the
1078 corresponding address field.
1079 """
1080 data = {'MachineId': '0', 'Name': name, 'Status': status}
1081 if address is not None:
1082 data['PublicAddress'] = address
1083 return 'change', data
1084
1090 # The following group of tests exercises both the function return value and1085 # The following group of tests exercises both the function return value and
1091 # the function output, even if the output is handled by sub-functions.1086 # the function output, even if the output is handled by sub-functions.
1092 # This is done to simulate the different user experiences of observing the1087 # This is done to simulate the different user experiences of observing the
@@ -1096,20 +1091,46 @@
1096 # The glorious moments in the unit's life are properly highlighted.1091 # The glorious moments in the unit's life are properly highlighted.
1097 # The machine achievements are also celebrated.1092 # The machine achievements are also celebrated.
1098 env = self.make_env([1093 env = self.make_env([
1099 ([self.change_unit_pending], [self.change_machine_pending]),1094 ([self.make_unit_change('pending', address='')],
1100 ([], [self.change_machine_started]),1095 [self.make_machine_change('pending')]),
1101 ([self.change_unit_reachable], []),1096 ([], [self.make_machine_change('started')]),
1102 ([self.change_unit_installed], []),1097 ([self.make_unit_change('pending', address=self.address)], []),
1103 ([self.change_unit_started], []),1098 ([self.make_unit_change('installed', address=self.address)], []),
1104 ])1099 ([self.make_unit_change('started', address=self.address)], []),
1105 address = app.watch(env, 'django/42')1100 ])
1106 self.assertEqual(self.address, address)1101 address = app.watch(env, 'django/42')
1107 self.assertEqual(6, mock_print.call_count)1102 self.assertEqual(self.address, address)
1108 mock_print.assert_has_calls([1103 self.assertEqual(6, mock_print.call_count)
1109 self.unit_pending_call,1104 mock_print.assert_has_calls([
1110 self.machine_pending_call,1105 self.unit_pending_call,
1111 self.machine_started_call,1106 self.machine_pending_call,
1112 self.unit_reachable_call,1107 self.machine_started_call,
1108 self.unit_placed_unit_call,
1109 self.unit_installed_call,
1110 self.unit_started_call,
1111 ])
1112
1113 def test_unit_life_with_machine_address(self, mock_print):
1114 # The glorious moments in the unit's life are properly highlighted.
1115 # The machine achievements are also celebrated.
1116 # This time the new mega-watcher behavior is simulated, in which
1117 # addresses are included in the machine change.
1118 env = self.make_env([
1119 ([self.make_unit_change('pending')],
1120 [self.make_machine_change('pending', address='')]),
1121 ([], [self.make_machine_change('started', address=self.address)]),
1122 ([self.make_unit_change('pending')], []),
1123 ([self.make_unit_change('installed')], []),
1124 ([self.make_unit_change('started')], []),
1125 ])
1126 address = app.watch(env, 'django/42')
1127 self.assertEqual(self.address, address)
1128 self.assertEqual(6, mock_print.call_count)
1129 mock_print.assert_has_calls([
1130 self.unit_pending_call,
1131 self.machine_pending_call,
1132 self.unit_placed_machine_call,
1133 self.machine_started_call,
1113 self.unit_installed_call,1134 self.unit_installed_call,
1114 self.unit_started_call,1135 self.unit_started_call,
1115 ])1136 ])
@@ -1120,17 +1141,18 @@
1120 # The unit is first reachable and then pending. The machine starts1141 # The unit is first reachable and then pending. The machine starts
1121 # when the unit is already installed. All of this makes no sense1142 # when the unit is already installed. All of this makes no sense
1122 # and should never happen, but if it does, we deal with it.1143 # and should never happen, but if it does, we deal with it.
1123 ([self.change_unit_reachable], []),1144 ([self.make_unit_change('pending', address=self.address)], []),
1124 ([self.change_unit_pending], [self.change_machine_pending]),1145 ([self.make_unit_change('pending', address='')],
1125 ([self.change_unit_installed], []),1146 [self.make_machine_change('pending')]),
1126 ([], [self.change_machine_started]),1147 ([self.make_unit_change('installed', address=self.address)], []),
1127 ([self.change_unit_started], []),1148 ([], [self.make_machine_change('started')]),
1149 ([self.make_unit_change('started', address=self.address)], []),
1128 ])1150 ])
1129 address = app.watch(env, 'django/42')1151 address = app.watch(env, 'django/42')
1130 self.assertEqual(self.address, address)1152 self.assertEqual(self.address, address)
1131 self.assertEqual(6, mock_print.call_count)1153 self.assertEqual(6, mock_print.call_count)
1132 mock_print.assert_has_calls([1154 mock_print.assert_has_calls([
1133 self.unit_reachable_call,1155 self.unit_placed_unit_call,
1134 self.unit_pending_call,1156 self.unit_pending_call,
1135 self.machine_pending_call,1157 self.machine_pending_call,
1136 self.unit_installed_call,1158 self.unit_installed_call,
@@ -1139,31 +1161,72 @@
1139 ])1161 ])
11401162
1141 def test_missing_changes(self, mock_print):1163 def test_missing_changes(self, mock_print):
1142 # Only the unit started change is strictly required.1164 # Only the unit started change is strictly required when the unit
1143 env = self.make_env([([self.change_unit_started], [])])1165 # change includes the public address.
1166 env = self.make_env([
1167 ([self.make_unit_change('started', address=self.address)], []),
1168 ])
1144 address = app.watch(env, 'django/42')1169 address = app.watch(env, 'django/42')
1145 self.assertEqual(self.address, address)1170 self.assertEqual(self.address, address)
1146 self.assertEqual(2, mock_print.call_count)1171 self.assertEqual(2, mock_print.call_count)
1147 mock_print.assert_has_calls([1172 mock_print.assert_has_calls([
1148 self.unit_reachable_call,1173 self.unit_placed_unit_call,
1149 self.unit_started_call,1174 self.unit_started_call,
1175 ])
1176
1177 def test_missing_changes_with_machine_address(self, mock_print):
1178 # When using the new mega-watcher, a machine change including its
1179 # public address is also required.
1180 env = self.make_env([
1181 ([self.make_unit_change('started')], []),
1182 ([], [self.make_machine_change('started', address=self.address)]),
1183 ])
1184 address = app.watch(env, 'django/42')
1185 self.assertEqual(self.address, address)
1186 self.assertEqual(3, mock_print.call_count)
1187 mock_print.assert_has_calls([
1188 self.unit_started_call,
1189 self.unit_placed_machine_call,
1190 self.machine_started_call,
1150 ])1191 ])
11511192
1152 def test_ignored_machine_changes(self, mock_print):1193 def test_ignored_machine_changes(self, mock_print):
1153 # All machine changes are ignored until the application knows what1194 # All machine changes are ignored until the application knows what
1154 # machine the unit belongs to.1195 # machine the unit belongs to.
1155 env = self.make_env([1196 env = self.make_env([
1156 ([], [self.change_machine_pending]),1197 ([], [self.make_machine_change('pending')]),
1157 ([], [self.change_machine_started]),1198 ([], [self.make_machine_change('started')]),
1158 ([self.change_unit_started], []),1199 ([self.make_unit_change('started', address=self.address)], []),
1159 ])1200 ])
1160 address = app.watch(env, 'django/42')1201 address = app.watch(env, 'django/42')
1161 self.assertEqual(self.address, address)1202 self.assertEqual(self.address, address)
1162 # No machine related messages have been printed.1203 # No machine related messages have been printed.
1163 self.assertEqual(2, mock_print.call_count)1204 self.assertEqual(2, mock_print.call_count)
1164 mock_print.assert_has_calls([1205 mock_print.assert_has_calls([
1165 self.unit_reachable_call,1206 self.unit_placed_unit_call,
1166 self.unit_started_call,1207 self.unit_started_call,
1208 ])
1209
1210 def test_ignored_machine_changes_with_machine_address(self, mock_print):
1211 # All machine changes are ignored until the application knows what
1212 # machine the unit belongs to. When the above happens, previously
1213 # collected machine changes are still parsed in the case the address
1214 # is not yet known.
1215 env = self.make_env([
1216 ([], [self.make_machine_change('pending')]),
1217 ([],
1218 [self.make_machine_change('installed', address=self.address)]),
1219 ([], [self.make_machine_change('started', address=self.address)]),
1220 ([self.make_unit_change('started')], []),
1221 ])
1222 address = app.watch(env, 'django/42')
1223 self.assertEqual(self.address, address)
1224 # No machine related messages have been printed.
1225 self.assertEqual(3, mock_print.call_count)
1226 mock_print.assert_has_calls([
1227 self.unit_started_call,
1228 self.unit_placed_machine_call,
1229 self.machine_started_call,
1167 ])1230 ])
11681231
1169 def test_unit_already_deployed(self, mock_print):1232 def test_unit_already_deployed(self, mock_print):
@@ -1171,30 +1234,76 @@
1171 # This happens, e.g., when executing Quickstart a second time, and both1234 # This happens, e.g., when executing Quickstart a second time, and both
1172 # the unit and the machine are already started.1235 # the unit and the machine are already started.
1173 env = self.make_env([1236 env = self.make_env([
1174 ([self.change_unit_started], [self.change_machine_started]),1237 ([self.make_unit_change('started', address=self.address)],
1238 [self.make_machine_change('started')]),
1175 ])1239 ])
1176 address = app.watch(env, 'django/42')1240 address = app.watch(env, 'django/42')
1177 self.assertEqual(self.address, address)1241 self.assertEqual(self.address, address)
1178 self.assertEqual(2, mock_print.call_count)1242 self.assertEqual(2, mock_print.call_count)
11791243
1244 def test_unit_already_deployed_with_machine_address(self, mock_print):
1245 # Simulate the unit we are observing has been already deployed.
1246 # This happens, e.g., when executing Quickstart a second time, and both
1247 # the unit and the machine are already started.
1248 # This time the new mega-watcher behavior is simulated, in which
1249 # addresses are included in the machine change.
1250 env = self.make_env([
1251 ([self.make_unit_change('started')],
1252 [self.make_machine_change('started', address=self.address)]),
1253 ])
1254 address = app.watch(env, 'django/42')
1255 self.assertEqual(self.address, address)
1256 self.assertEqual(3, mock_print.call_count)
1257 mock_print.assert_has_calls([
1258 self.unit_started_call,
1259 self.unit_placed_machine_call,
1260 self.machine_started_call,
1261 ])
1262
1180 def test_machine_already_started(self, mock_print):1263 def test_machine_already_started(self, mock_print):
1181 # Simulate the unit is being deployed on an already started machine.1264 # Simulate the unit is being deployed on an already started machine.
1182 # This happens, e.g., when running Quickstart on a non-local1265 # This happens, e.g., when running Quickstart on a non-local
1183 # environment type: the unit is deployed on the bootstrap node, which1266 # environment type: the unit is deployed on the bootstrap node, which
1184 # is assumed to be started.1267 # is assumed to be started.
1185 env = self.make_env([1268 env = self.make_env([
1186 ([self.change_unit_pending], [self.change_machine_started]),1269 ([self.make_unit_change('pending', address='')],
1187 ([self.change_unit_reachable], []),1270 [self.make_machine_change('started')]),
1188 ([self.change_unit_installed], []),1271 ([self.make_unit_change('pending', address=self.address)], []),
1189 ([self.change_unit_started], []),1272 ([self.make_unit_change('installed', address=self.address)], []),
1190 ])1273 ([self.make_unit_change('started', address=self.address)], []),
1191 address = app.watch(env, 'django/42')1274 ])
1192 self.assertEqual(self.address, address)1275 address = app.watch(env, 'django/42')
1193 self.assertEqual(5, mock_print.call_count)1276 self.assertEqual(self.address, address)
1194 mock_print.assert_has_calls([1277 self.assertEqual(5, mock_print.call_count)
1195 self.unit_pending_call,1278 mock_print.assert_has_calls([
1196 self.machine_started_call,1279 self.unit_pending_call,
1197 self.unit_reachable_call,1280 self.machine_started_call,
1281 self.unit_placed_unit_call,
1282 self.unit_installed_call,
1283 self.unit_started_call,
1284 ])
1285
1286 def test_machine_already_started_with_machine_address(self, mock_print):
1287 # Simulate the unit is being deployed on an already started machine.
1288 # This happens, e.g., when running Quickstart on a non-local
1289 # environment type: the unit is deployed on the bootstrap node, which
1290 # is assumed to be started.
1291 # This time the new mega-watcher behavior is simulated, in which
1292 # addresses are included in the machine change.
1293 env = self.make_env([
1294 ([self.make_unit_change('pending')],
1295 [self.make_machine_change('started', address=self.address)]),
1296 ([self.make_unit_change('pending')], []),
1297 ([self.make_unit_change('installed')], []),
1298 ([self.make_unit_change('started')], []),
1299 ])
1300 address = app.watch(env, 'django/42')
1301 self.assertEqual(self.address, address)
1302 self.assertEqual(5, mock_print.call_count)
1303 mock_print.assert_has_calls([
1304 self.unit_pending_call,
1305 self.unit_placed_machine_call,
1306 self.machine_started_call,
1198 self.unit_installed_call,1307 self.unit_installed_call,
1199 self.unit_started_call,1308 self.unit_started_call,
1200 ])1309 ])
@@ -1203,38 +1312,25 @@
1203 # Changes to units or machines we are not observing are ignored. Also1312 # Changes to units or machines we are not observing are ignored. Also
1204 # ensure that repeated changes to a single entity are ignored, even if1313 # ensure that repeated changes to a single entity are ignored, even if
1205 # they are unlikely to happen.1314 # they are unlikely to happen.
1206 change_another_machine_pending = ('change', {1315 pending_unit_change = self.make_unit_change('pending', address='')
1207 'Id': '42',1316 started_unit_change = self.make_unit_change(
1208 'Status': 'pending',1317 'started', address=self.address)
1209 })
1210 change_another_machine_started = ('change', {
1211 'Id': '1',
1212 'Status': 'started',
1213 })
1214 change_another_unit_pending = ('change', {
1215 'MachineId': '0',
1216 'Name': 'haproxy/0',
1217 'Status': 'pending',
1218 })
1219 change_another_unit_started = ('change', {
1220 'MachineId': '0',
1221 'Name': 'haproxy/0',
1222 'Status': 'started',
1223 })
1224 env = self.make_env([1318 env = self.make_env([
1225 # Add a repeated change.1319 # Add a repeated change.
1226 ([self.change_unit_pending, self.change_unit_pending],1320 ([pending_unit_change, pending_unit_change],
1227 [self.change_machine_pending]),1321 [self.make_machine_change('pending')]),
1228 # Add extraneous unit and machine changes.1322 # Add extraneous unit and machine changes.
1229 ([change_another_unit_pending], [change_another_machine_pending]),1323 ([self.make_unit_change('pending', name='haproxy/0')],
1324 [self.make_machine_change('pending', name='42')]),
1230 # Add a change to an extraneous machine.1325 # Add a change to an extraneous machine.
1231 ([], [change_another_machine_started,1326 ([], [self.make_machine_change('started', name='42'),
1232 self.change_machine_started]),1327 self.make_machine_change('started')]),
1233 # Add a change to an extraneous unit.1328 # Add a change to an extraneous unit.
1234 ([change_another_unit_started, self.change_unit_reachable], []),1329 ([self.make_unit_change('started', name='haproxy/0'),
1235 ([self.change_unit_installed], []),1330 self.make_unit_change('pending', address=self.address)], []),
1331 ([self.make_unit_change('installed', address=self.address)], []),
1236 # Add another repeated change.1332 # Add another repeated change.
1237 ([self.change_unit_started, self.change_unit_started], []),1333 ([started_unit_change, started_unit_change], []),
1238 ])1334 ])
1239 address = app.watch(env, 'django/42')1335 address = app.watch(env, 'django/42')
1240 self.assertEqual(self.address, address)1336 self.assertEqual(self.address, address)
@@ -1243,7 +1339,7 @@
1243 self.unit_pending_call,1339 self.unit_pending_call,
1244 self.machine_pending_call,1340 self.machine_pending_call,
1245 self.machine_started_call,1341 self.machine_started_call,
1246 self.unit_reachable_call,1342 self.unit_placed_unit_call,
1247 self.unit_installed_call,1343 self.unit_installed_call,
1248 self.unit_started_call,1344 self.unit_started_call,
1249 ])1345 ])
@@ -1251,7 +1347,7 @@
1251 def test_api_error(self, mock_print):1347 def test_api_error(self, mock_print):
1252 # A ProgramExit is raised if an error occurs in one of the API calls.1348 # A ProgramExit is raised if an error occurs in one of the API calls.
1253 env = self.make_env([1349 env = self.make_env([
1254 ([self.change_unit_pending], []),1350 ([self.make_unit_change('pending', address='')], []),
1255 self.make_env_error('next returned an error'),1351 self.make_env_error('next returned an error'),
1256 ])1352 ])
1257 expected = 'bad API server response: next returned an error'1353 expected = 'bad API server response: next returned an error'
@@ -1262,13 +1358,15 @@
12621358
1263 def test_other_errors(self, mock_print):1359 def test_other_errors(self, mock_print):
1264 # Any other errors occurred during the process are not trapped.1360 # Any other errors occurred during the process are not trapped.
1265 error = ValueError('explode!')1361 env = self.make_env([
1266 env = self.make_env([([self.change_unit_installed], []), error])1362 ([self.make_unit_change('installed', address=self.address)], []),
1363 ValueError('explode!'),
1364 ])
1267 with self.assert_value_error('explode!'):1365 with self.assert_value_error('explode!'):
1268 app.watch(env, 'django/42')1366 app.watch(env, 'django/42')
1269 self.assertEqual(2, mock_print.call_count)1367 self.assertEqual(2, mock_print.call_count)
1270 mock_print.assert_has_calls([1368 mock_print.assert_has_calls([
1271 self.unit_reachable_call, self.unit_installed_call])1369 self.unit_placed_unit_call, self.unit_installed_call])
12721370
1273 def test_machine_status_error(self, mock_print):1371 def test_machine_status_error(self, mock_print):
1274 # A ProgramExit is raised if an the machine is found in an error state.1372 # A ProgramExit is raised if an the machine is found in an error state.
@@ -1277,10 +1375,12 @@
1277 'Status': 'error',1375 'Status': 'error',
1278 'StatusInfo': 'oddities',1376 'StatusInfo': 'oddities',
1279 })1377 })
1378 self.make_machine_change('error')
1280 # The unit pending change is required to make the function know which1379 # The unit pending change is required to make the function know which
1281 # machine to observe.1380 # machine to observe.
1282 env = self.make_env([(1381 env = self.make_env([
1283 [self.change_unit_pending], [change_machine_error]),1382 ([self.make_unit_change('pending', address='')],
1383 [change_machine_error]),
1284 ])1384 ])
1285 expected = 'machine 0 is in an error state: error: oddities'1385 expected = 'machine 0 is in an error state: error: oddities'
1286 with self.assert_program_exit(expected):1386 with self.assert_program_exit(expected):
12871387
=== modified file 'quickstart/tests/test_watchers.py'
--- quickstart/tests/test_watchers.py 2014-01-17 13:54:51 +0000
+++ quickstart/tests/test_watchers.py 2014-04-07 13:24:46 +0000
@@ -26,46 +26,202 @@
26from quickstart.tests import helpers26from quickstart.tests import helpers
2727
2828
29# Define addresses to be used in tests.
30cloud_addresses = [
31 {'NetworkName': '',
32 'NetworkScope': 'public',
33 'Type': 'hostname',
34 'Value': 'eu-west-1.compute.example.com'},
35 {'NetworkName': '',
36 'NetworkScope': 'local-cloud',
37 'Type': 'hostname',
38 'Value': 'eu-west-1.example.internal'},
39 {'NetworkName': '',
40 'NetworkScope': 'public',
41 'Type': 'ipv4',
42 'Value': '444.222.444.222'},
43 {'NetworkName': '',
44 'NetworkScope': 'local-cloud',
45 'Type': 'ipv4',
46 'Value': '10.42.47.10'},
47 {'NetworkName': '',
48 'NetworkScope': '',
49 'Type': 'ipv6',
50 'Value': 'fe80::92b8:d0ff:fe94:8f8c'},
51]
52container_addresses = [
53 {'NetworkName': '',
54 'NetworkScope': '',
55 'Type': 'ipv4',
56 'Value': '10.0.3.42'},
57 {'NetworkName': '',
58 'NetworkScope': '',
59 'Type': 'ipv6',
60 'Value': 'fe80::216:3eff:fefd:787e'},
61]
62
63
64class TestRetrievePublicAddress(unittest.TestCase):
65
66 def test_empty_addresses(self):
67 # None is returned if there are no available addresses.
68 self.assertIsNone(watchers.retrieve_public_adddress([]))
69
70 def test_cloud_address_not_found(self):
71 # None is returned if a cloud machine public address is not available.
72 addresses = [
73 {'NetworkName': '',
74 'NetworkScope': 'local-cloud',
75 'Type': 'hostname',
76 'Value': 'eu-west-1.example.internal'},
77 {'NetworkName': '',
78 'NetworkScope': 'local-cloud',
79 'Type': 'ipv4',
80 'Value': '10.42.47.10'},
81 ]
82 self.assertIsNone(watchers.retrieve_public_adddress(addresses))
83
84 def test_container_address_not_found(self):
85 # None is returned if an LXC public address is not available.
86 addresses = [{
87 'NetworkName': '',
88 'NetworkScope': '',
89 'Type': 'ipv6',
90 'Value': 'fe80::216:3eff:fefd:787e',
91 }]
92 self.assertIsNone(watchers.retrieve_public_adddress(addresses))
93
94 def test_empty_public_address(self):
95 # None is returned if the public address has no value.
96 addresses = [
97 {'NetworkName': '',
98 'NetworkScope': 'local-cloud',
99 'Type': 'hostname',
100 'Value': 'eu-west-1.example.internal'},
101 {'NetworkName': '',
102 'NetworkScope': 'public',
103 'Type': 'ipv4',
104 'Value': ''},
105 ]
106 self.assertIsNone(watchers.retrieve_public_adddress(addresses))
107
108 def test_cloud_addresses(self):
109 # The public address of a cloud machine is properly returned.
110 public_address = watchers.retrieve_public_adddress(cloud_addresses)
111 self.assertEqual('eu-west-1.compute.example.com', public_address)
112
113 def test_container_addresses(self):
114 # The public address of an LXC instance is properly returned.
115 public_address = watchers.retrieve_public_adddress(container_addresses)
116 self.assertEqual('10.0.3.42', public_address)
117
118 def test_last_unknown_address(self):
119 # If the scope of multiple addresses is unknown, the last one is taken.
120 addresses = [
121 {'NetworkName': '',
122 'NetworkScope': '',
123 'Type': 'ipv4',
124 'Value': '10.0.3.42'},
125 {'NetworkName': '',
126 'NetworkScope': '',
127 'Type': 'ipv4',
128 'Value': '10.0.3.47'},
129 ]
130 public_address = watchers.retrieve_public_adddress(addresses)
131 self.assertEqual('10.0.3.47', public_address)
132
133
29class TestParseMachineChange(helpers.ValueErrorTestsMixin, unittest.TestCase):134class TestParseMachineChange(helpers.ValueErrorTestsMixin, unittest.TestCase):
30135
31 def test_machine_removed(self):136 def test_machine_removed(self):
32 # A ValueError is raised if the change represents a machine removal.137 # A ValueError is raised if the change represents a machine removal.
33 data = {'Id': '1', 'Status': 'started'}138 data = {'Addresses': [], 'Id': '1', 'Status': 'started'}
34 with self.assert_value_error('machine 1 unexpectedly removed'):139 with self.assert_value_error('machine 1 unexpectedly removed'):
35 watchers.parse_machine_change('remove', data, '')140 watchers.parse_machine_change('remove', data, '', '')
36141
37 def test_machine_error(self):142 def test_machine_error(self):
38 # A ValueError is raised if the machine is in an error state.143 # A ValueError is raised if the machine is in an error state.
39 data = {'Id': '1', 'Status': 'error', 'StatusInfo': 'bad wolf'}144 data = {
145 'Addresses': [],
146 'Id': '1',
147 'Status': 'error',
148 'StatusInfo': 'bad wolf',
149 }
40 expected_error = 'machine 1 is in an error state: error: bad wolf'150 expected_error = 'machine 1 is in an error state: error: bad wolf'
41 with self.assert_value_error(expected_error):151 with self.assert_value_error(expected_error):
42 watchers.parse_machine_change('change', data, '')152 watchers.parse_machine_change('change', data, '', '')
43153
44 @helpers.mock_print154 @helpers.mock_print
45 def test_pending_status_notified(self, mock_print):155 def test_pending_status_notified(self, mock_print):
46 # A message is printed to stdout when the machine changes its status156 # A message is printed to stdout when the machine changes its status
47 # to "pending". The new status is also returned by the function.157 # to "pending". The new status is also returned by the function.
48 data = {'Id': '1', 'Status': 'pending'}158 data = {'Addresses': [], 'Id': '1', 'Status': 'pending'}
49 status = watchers.parse_machine_change('change', data, '')159 status, address = watchers.parse_machine_change('change', data, '', '')
50 self.assertEqual('pending', status)160 self.assertEqual('pending', status)
161 self.assertEqual('', address)
51 mock_print.assert_called_once_with('machine 1 provisioning is pending')162 mock_print.assert_called_once_with('machine 1 provisioning is pending')
52163
53 @helpers.mock_print164 @helpers.mock_print
54 def test_started_status_notified(self, mock_print):165 def test_started_status_notified(self, mock_print):
55 # A message is printed to stdout when the machine changes its status166 # A message is printed to stdout when the machine changes its status
56 # to "started". The new status is also returned by the function.167 # to "started". The new status is also returned by the function.
57 data = {'Id': '42', 'Status': 'started'}168 data = {'Addresses': [], 'Id': '42', 'Status': 'started'}
58 status = watchers.parse_machine_change('change', data, 'pending')169 status, address = watchers.parse_machine_change(
170 'change', data, 'pending', '')
59 self.assertEqual('started', status)171 self.assertEqual('started', status)
172 self.assertEqual('', address)
60 mock_print.assert_called_once_with('machine 42 is started')173 mock_print.assert_called_once_with('machine 42 is started')
61174
62 @helpers.mock_print175 @helpers.mock_print
63 def test_status_not_changed(self, mock_print):176 def test_status_not_changed(self, mock_print):
64 # If the status in the machine change and the given current status are177 # If the status in the machine change and the given current status are
65 # the same value, nothing is printed and the status is returned.178 # the same value, nothing is printed and the status is returned.
179 data = {'Addresses': [], 'Id': '47', 'Status': 'pending'}
180 status, address = watchers.parse_machine_change(
181 'change', data, 'pending', '')
182 self.assertEqual('pending', status)
183 self.assertEqual('', address)
184 self.assertFalse(mock_print.called)
185
186 @helpers.mock_print
187 def test_address_notified(self, mock_print):
188 # A message is printed to stdout when the machine obtains a public
189 # address.
190 data = {'Addresses': cloud_addresses, 'Id': '1', 'Status': 'pending'}
191 status, address = watchers.parse_machine_change(
192 'change', data, 'pending', '')
193 self.assertEqual('pending', status)
194 self.assertEqual('eu-west-1.compute.example.com', address)
195 mock_print.assert_called_once_with(
196 'unit placed on eu-west-1.compute.example.com')
197
198 @helpers.mock_print
199 def test_both_status_and_address_notified(self, mock_print):
200 # Both status and public address changes are notified if required.
201 data = {
202 'Addresses': container_addresses,
203 'Id': '0',
204 'Status': 'started',
205 }
206 status, address = watchers.parse_machine_change(
207 'change', data, 'pending', '')
208 self.assertEqual('started', status)
209 self.assertEqual('10.0.3.42', address)
210 self.assertEqual(2, mock_print.call_count)
211 mock_print.assert_has_calls([
212 mock.call('unit placed on 10.0.3.42'),
213 mock.call('machine 0 is started'),
214 ])
215
216 @helpers.mock_print
217 def test_address_not_available(self, mock_print):
218 # An empty address is returned when the addresses field is not
219 # included in the change data.
66 data = {'Id': '47', 'Status': 'pending'}220 data = {'Id': '47', 'Status': 'pending'}
67 status = watchers.parse_machine_change('change', data, 'pending')221 status, address = watchers.parse_machine_change(
222 'change', data, 'pending', '')
68 self.assertEqual('pending', status)223 self.assertEqual('pending', status)
224 self.assertEqual('', address)
69 self.assertFalse(mock_print.called)225 self.assertFalse(mock_print.called)
70226
71227
@@ -107,7 +263,7 @@
107 self.assertEqual('haproxy2.example.com', address)263 self.assertEqual('haproxy2.example.com', address)
108 self.assertEqual('42', machine_id)264 self.assertEqual('42', machine_id)
109 mock_print.assert_called_once_with(265 mock_print.assert_called_once_with(
110 'setting up haproxy/2 on haproxy2.example.com')266 'haproxy/2 placed on haproxy2.example.com')
111267
112 @helpers.mock_print268 @helpers.mock_print
113 def test_pending_status_notified(self, mock_print):269 def test_pending_status_notified(self, mock_print):
@@ -173,7 +329,7 @@
173 watchers.parse_unit_change('change', data, '', '')329 watchers.parse_unit_change('change', data, '', '')
174 self.assertEqual(2, mock_print.call_count)330 self.assertEqual(2, mock_print.call_count)
175 mock_print.assert_has_calls([331 mock_print.assert_has_calls([
176 mock.call('setting up django/0 on django42.example.com'),332 mock.call('django/0 placed on django42.example.com'),
177 mock.call('django/0 is ready on machine 0'),333 mock.call('django/0 is ready on machine 0'),
178 ])334 ])
179335
@@ -189,6 +345,18 @@
189 self.assertEqual('', machine_id)345 self.assertEqual('', machine_id)
190 self.assertFalse(mock_print.called)346 self.assertFalse(mock_print.called)
191347
348 @helpers.mock_print
349 def test_address_not_available(self, mock_print):
350 # An empty address is returned when the public address field is not
351 # included in the change data.
352 data = {'Name': 'haproxy/2', 'Status': 'pending', 'MachineId': '42'}
353 status, address, machine_id = watchers.parse_unit_change(
354 'change', data, 'pending', '')
355 self.assertEqual('pending', status)
356 self.assertEqual('', address)
357 self.assertEqual('42', machine_id)
358 self.assertFalse(mock_print.called)
359
192360
193class TestUnitMachineChanges(unittest.TestCase):361class TestUnitMachineChanges(unittest.TestCase):
194362
195363
=== modified file 'quickstart/watchers.py'
--- quickstart/watchers.py 2014-01-17 10:35:36 +0000
+++ quickstart/watchers.py 2014-04-07 13:24:46 +0000
@@ -22,16 +22,85 @@
22)22)
2323
2424
25def parse_machine_change(action, data, current_status):25IPV6_ADDRESS = 'ipv6'
26NETWORK_PUBLIC = 'public'
27NETWORK_UNKNOWN = ''
28
29
30def retrieve_public_adddress(addresses):
31 """Parse the given addresses and return a public address if available.
32
33 The addresses argument is a list of address dictionaries.
34 Cloud addresses look like the following:
35
36 [
37 {'NetworkName': '',
38 'NetworkScope': 'public',
39 'Type': 'hostname',
40 'Value': 'eu-west-1.compute.example.com'},
41 {'NetworkName': '',
42 'NetworkScope': 'local-cloud',
43 'Type': 'hostname',
44 'Value': 'eu-west-1.example.internal'},
45 {'NetworkName': '',
46 'NetworkScope': 'public',
47 'Type': 'ipv4',
48 'Value': '444.222.444.222'},
49 {'NetworkName': '',
50 'NetworkScope': 'local-cloud',
51 'Type': 'ipv4',
52 'Value': '10.42.47.10'},
53 {'NetworkName': '',
54 'NetworkScope': '',
55 'Type': 'ipv6',
56 'Value': 'fe80::92b8:d0ff:fe94:8f8c'},
57 ]
58
59 When using the local provider, LXC addresses are like the following:
60
61 [
62 {'NetworkName': '',
63 'NetworkScope': '',
64 'Type': 'ipv4',
65 'Value': '10.0.3.42'},
66 {'NetworkName': '',
67 'NetworkScope': '',
68 'Type': 'ipv6',
69 'Value': 'fe80::216:3eff:fefd:787e'},
70 ]
71
72 If the addresses list is empty, or if no public/reachable addresses can be
73 found, this function returns None.
74 """
75 # This implementation reflects how the public address is retrieved in Juju:
76 # see juju-core/instance/address.go:SelectPublicAddress.
77 public_address = None
78 for address in addresses:
79 value = address['Value']
80 # Exclude empty values and ipv6 addresses.
81 if value and (address['Type'] != IPV6_ADDRESS):
82 scope = address['NetworkScope']
83 # If the scope is public then we have found the address.
84 if scope == NETWORK_PUBLIC:
85 return value
86 # If the scope is unknown then store the value. This way the last
87 # address with unknown scope will be returned, and we are able to
88 # return the right LXC address.
89 if scope == NETWORK_UNKNOWN:
90 public_address = value
91 return public_address
92
93
94def parse_machine_change(action, data, current_status, address):
26 """Parse the given machine change.95 """Parse the given machine change.
2796
28 The change is represented by the given action/data pair.97 The change is represented by the given action/data pair.
29 Also receive the last known machine status, which can be an empty string98 Also receive the last known machine status and address, which can be empty
30 if the information is unknown.99 strings if those pieces of information are unknown.
31100
32 Output a human readable message each time a relevant change is found.101 Output a human readable message each time a relevant change is found.
33102
34 Return the machine status.103 Return the machine status and the address.
35 Raise a ValueError if the machine is removed or in an error state.104 Raise a ValueError if the machine is removed or in an error state.
36 """105 """
37 machine_id = data['Id']106 machine_id = data['Id']
@@ -44,6 +113,16 @@
44 msg = 'machine {} is in an error state: {}: {}'.format(113 msg = 'machine {} is in an error state: {}: {}'.format(
45 machine_id, status, data['StatusInfo'])114 machine_id, status, data['StatusInfo'])
46 raise ValueError(msg.encode('utf-8'))115 raise ValueError(msg.encode('utf-8'))
116 # Notify when the machine becomes reachable. Starting from juju-core 1.18,
117 # the mega-watcher for machines includes addresses for each machine. This
118 # info is the preferred source where to look to retrieve the public address
119 # of units hosted by a specific machine.
120 if not address:
121 addresses = data.get('Addresses', [])
122 public_address = retrieve_public_adddress(addresses)
123 if public_address is not None:
124 address = public_address
125 print('unit placed on {}'.format(address))
47 # Notify status changes.126 # Notify status changes.
48 if status != current_status:127 if status != current_status:
49 if status == 'pending':128 if status == 'pending':
@@ -51,7 +130,7 @@
51 machine_id))130 machine_id))
52 elif status == 'started':131 elif status == 'started':
53 print('machine {} is started'.format(machine_id))132 print('machine {} is started'.format(machine_id))
54 return status133 return status, address
55134
56135
57def parse_unit_change(action, data, current_status, address):136def parse_unit_change(action, data, current_status, address):
@@ -77,18 +156,22 @@
77 msg = '{} is in an error state: {}: {}'.format(156 msg = '{} is in an error state: {}: {}'.format(
78 unit_name, status, data['StatusInfo'])157 unit_name, status, data['StatusInfo'])
79 raise ValueError(msg.encode('utf-8'))158 raise ValueError(msg.encode('utf-8'))
80 # Notify when the unit becomes reachable.159 # Notify when the unit becomes reachable. Up to juju-core 1.18, the
160 # mega-watcher for units includes the public address for each unit. This
161 # info is likely to be deprecated in favor of addresses as included in the
162 # mega-watcher for machines, but we still try to retrieve the address here
163 # for backward compatibility.
81 if not address:164 if not address:
82 address = data['PublicAddress']165 address = data.get('PublicAddress', '')
83 if address:166 if address:
84 print('setting up {} on {}'.format(unit_name, address))167 print('{} placed on {}'.format(unit_name, address))
85 # Notify status changes.168 # Notify status changes.
86 if status != current_status:169 if status != current_status:
87 if status == 'pending':170 if status == 'pending':
88 print('{} deployment is pending'.format(unit_name))171 print('{} deployment is pending'.format(unit_name))
89 elif status == 'installed':172 elif status == 'installed':
90 print('{} is installed'.format(unit_name))173 print('{} is installed'.format(unit_name))
91 elif address and status == 'started':174 elif status == 'started':
92 print('{} is ready on machine {}'.format(175 print('{} is ready on machine {}'.format(
93 unit_name, data['MachineId']))176 unit_name, data['MachineId']))
94 return status, address, data.get('MachineId', '')177 return status, address, data.get('MachineId', '')

Subscribers

People subscribed via source and target branches