Merge lp:~frankban/juju-quickstart/manual-provider into lp:juju-quickstart

Proposed by Francesco Banconi
Status: Merged
Merged at revision: 106
Proposed branch: lp:~frankban/juju-quickstart/manual-provider
Merge into: lp:juju-quickstart
Diff against target: 439 lines (+143/-76)
3 files modified
quickstart/models/envs.py (+100/-44)
quickstart/tests/cli/test_views.py (+4/-4)
quickstart/tests/models/test_envs.py (+39/-28)
To merge this branch: bzr merge lp:~frankban/juju-quickstart/manual-provider
Reviewer Review Type Date Requested Status
Juju GUI Hackers Pending
Review via email: mp+241595@code.launchpad.net

Description of the change

Add support for the manual provider.

Also reorder the provider fields so
that required ones come first.
The hope for this is to improve the
user experience while creating
environments.

Tests: `make check`.

QA: use quickstart as usual to set up
environments.
To test the manual provider, I've done
the following:

- I used a separate vmware virtual machine:
  let's assume its address is 192.168.1.5.
- Install the SSH server in the vm:
        `sudo apt-get install ssh`.
- Copy your ssh credential over:
        `scp -rp ~/.ssh 192.168.1.5:~`.
- Allow sudo power without password for your user on the vm, by
  editing the /etc/sudoers file and adding the following:
        $USER ALL=(ALL) NOPASSWD: ALL
  where $USER is your user name.
- Run `.venv/bin/python juju-quickstart -i`, select the option to
  create a new "Manual Provisioning" environment, select a name for it and
  use the vm address as the "bootstrap host", save the environment
  and then run it!
- wait for the environment to be ready, then run quickstart again
  with the same environment to check it recognizes it's
  already bootstrapped.
Done, thank you!

https://codereview.appspot.com/172410043/

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

Reviewers: mp+241595_code.launchpad.net,

Message:
Please take a look.

Description:
Add support for the manual provider.

Also reorder the provider fields so
that required ones come first.
The hope for this is to improve the
user experience while creating
environments.

Tests: `make check`.

QA: use quickstart as usual to set up
environments.
To test the manual provider, I've done
the following:

- I used a separate vmware virtual machine:
   let's assume its address is 192.168.1.5.
- Install the SSH server in the vm:
         `sudo apt-get install ssh`.
- Copy your ssh credential over:
         `scp -rp ~/.ssh 192.168.1.5:~`.
- Allow sudo power without password for your user on the vm, by
   editing the /etc/sudoers file and adding the following:
         $USER ALL=(ALL) NOPASSWD: ALL
   where $USER is your user name.
- Run `.venv/bin/python juju-quickstart -i`, select the option to
   create a new "Manual Provisioning" environment, select a name for it
and
   use the vm address as the "bootstrap host", save the environment
   and then run it!
- wait for the environment to be ready, then run quickstart again
   with the same environment to check it recognizes it's
   already bootstrapped.
Done, thank you!

https://code.launchpad.net/~frankban/juju-quickstart/manual-provider/+merge/241595

(do not edit description out of merge proposal)

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

Affected files (+145, -76 lines):
   A [revision details]
   M quickstart/models/envs.py
   M quickstart/tests/cli/test_views.py
   M quickstart/tests/models/test_envs.py

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

LGTM and will QA. Ideally it would be cool if our QA instructions were
using guimaas to just bring up notes and then hook them into juju.

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

https://codereview.appspot.com/172410043/diff/1/quickstart/models/envs.py#newcode720
quickstart/models/envs.py:720: help='The host name of the machine where
the bootstrap '
name/ip address?

https://codereview.appspot.com/172410043/diff/1/quickstart/models/envs.py#newcode732
quickstart/models/envs.py:732: fields.IntField(
ugh, I think this is going away soon. I'll ping wallyworld and see if
it's in 1.21. If that's true it might be worth just waiting a few weeks
for 1.21 and skipping this.

https://codereview.appspot.com/172410043/

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

looks like the removal of provider storage won't be in 1.21 so ignore
that hope of not needing it.

https://codereview.appspot.com/172410043/

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

QA:

I used a digital ocean droplet

- created droplet with my ssh key
- had to ssh root@xxx to login and set an initial root password
- added the 'ocean' envirnoment using juju-quickstart -i
- failed at first as it said no tools for my environment so I retired
with --upload-tools
- that ran and worked, gui loaded up in a new window for me
- reran quickstart and it worked and loaded the gui window again for me.
Really pretty quick <3

So my one QA question is if we should always upload tools? I guess I'm
on 1.21-beta1 so maybe it's that vs the manual provider. Never mind.

https://codereview.appspot.com/172410043/

Revision history for this message
Madison Scott-Clary (makyo) wrote :

LGTM, no QA. THanks for this!

https://codereview.appspot.com/172410043/

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

Thanks for the reviews!

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

https://codereview.appspot.com/172410043/diff/1/quickstart/models/envs.py#newcode720
quickstart/models/envs.py:720: help='The host name of the machine where
the bootstrap '
On 2014/11/12 22:08:24, rharding wrote:
> name/ip address?

Done.

https://codereview.appspot.com/172410043/

115. By Francesco Banconi

Fix typo.

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

*** Submitted:

Add support for the manual provider.

Also reorder the provider fields so
that required ones come first.
The hope for this is to improve the
user experience while creating
environments.

Tests: `make check`.

QA: use quickstart as usual to set up
environments.
To test the manual provider, I've done
the following:

- I used a separate vmware virtual machine:
   let's assume its address is 192.168.1.5.
- Install the SSH server in the vm:
         `sudo apt-get install ssh`.
- Copy your ssh credential over:
         `scp -rp ~/.ssh 192.168.1.5:~`.
- Allow sudo power without password for your user on the vm, by
   editing the /etc/sudoers file and adding the following:
         $USER ALL=(ALL) NOPASSWD: ALL
   where $USER is your user name.
- Run `.venv/bin/python juju-quickstart -i`, select the option to
   create a new "Manual Provisioning" environment, select a name for it
and
   use the vm address as the "bootstrap host", save the environment
   and then run it!
- wait for the environment to be ready, then run quickstart again
   with the same environment to check it recognizes it's
   already bootstrapped.
Done, thank you!

R=rharding, matthew.scott
CC=
https://codereview.appspot.com/172410043

https://codereview.appspot.com/172410043/

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'quickstart/models/envs.py'
2--- quickstart/models/envs.py 2014-11-11 11:10:33 +0000
3+++ quickstart/models/envs.py 2014-11-13 08:56:33 +0000
4@@ -456,9 +456,8 @@
5 ),
6 'fields': (
7 provider_field,
8+ # Required fields.
9 name_field,
10- admin_secret_field,
11- default_series_field,
12 fields.PasswordField(
13 'access-key', label='access key', required=True,
14 help='The access key to use to authenticate to AWS. '
15@@ -475,12 +474,15 @@
16 'secret-key', label='secret key', required=True,
17 help='The secret key to use to authenticate to AWS. '
18 'See the access key help above.'),
19- fields.AutoGeneratedStringField(
20- 'control-bucket', label='control bucket', required=False,
21- help='the globally unique S3 bucket name'),
22+ # Optional fields.
23 fields.ChoiceField(
24 'region', choices=ec2_regions, default='us-east-1',
25 label='region', required=False, help='the ec2 region to use'),
26+ admin_secret_field,
27+ default_series_field,
28+ fields.AutoGeneratedStringField(
29+ 'control-bucket', label='control bucket', required=False,
30+ help='the globally unique S3 bucket name'),
31 is_default_field,
32 ),
33 }
34@@ -496,20 +498,8 @@
35 ),
36 'fields': (
37 provider_field,
38+ # Required fields.
39 name_field,
40- admin_secret_field,
41- default_series_field,
42- fields.BoolField(
43- 'use-floating-ip', label='use floating IP', allow_mixed=False,
44- required=True,
45- help='Specifies whether the use of a floating IP address is '
46- 'required to give the nodes a public IP address. '
47- 'Some installations assign public IP addresses by '
48- 'default without requiring a floating IP address. '
49- 'For HP Public Cloud, floating IP must be activated.'),
50- fields.AutoGeneratedStringField(
51- 'control-bucket', label='control bucket', required=False,
52- help='the globally unique swift bucket name'),
53 fields.SuggestionsStringField(
54 'auth-url', label='authentication URL', required=True,
55 help='The Keystone URL to use in the authentication process. '
56@@ -521,11 +511,20 @@
57 help='The OpenStack tenant name. For HP Public Cloud, this is '
58 'listed as the project name on the '
59 'https://horizon.hpcloud.com/landing/ page.'),
60+ fields.BoolField(
61+ 'use-floating-ip', label='use floating IP', allow_mixed=False,
62+ required=True,
63+ help='Specifies whether the use of a floating IP address is '
64+ 'required to give the nodes a public IP address. '
65+ 'Some installations assign public IP addresses by '
66+ 'default without requiring a floating IP address. '
67+ 'For HP Public Cloud, floating IP must be activated.'),
68 fields.SuggestionsStringField(
69 'region', label='region', required=True,
70 help='The OpenStack region to use. '
71 'For HP Public Cloud, use one of the following:',
72 suggestions=hp_regions),
73+ # Optional fields.
74 fields.ChoiceField(
75 'auth-mode', label='authentication mode', required=False,
76 default='userpass', choices=('userpass', 'keypair'),
77@@ -549,6 +548,11 @@
78 fields.PasswordField(
79 'secret-key', label='secret key', required=False,
80 help='the secret key to use for the keypair authentication'),
81+ fields.AutoGeneratedStringField(
82+ 'control-bucket', label='control bucket', required=False,
83+ help='the globally unique swift bucket name'),
84+ admin_secret_field,
85+ default_series_field,
86 is_default_field,
87 ),
88 }
89@@ -564,12 +568,8 @@
90 ),
91 'fields': (
92 provider_field,
93+ # Required fields.
94 name_field,
95- admin_secret_field,
96- default_series_field,
97- fields.ChoiceField(
98- 'location', choices=azure_locations, label='location',
99- required=True, help='the region to use'),
100 fields.StringField(
101 'management-subscription-id', required=True,
102 label='management subscription ID',
103@@ -590,6 +590,12 @@
104 'https://manage.windowsazure.com (Storage). '
105 'You must create the storage account in the same '
106 'region/location specified by the location key value.'),
107+ fields.ChoiceField(
108+ 'location', choices=azure_locations, label='location',
109+ required=True, help='the region to use'),
110+ # Optional fields.
111+ admin_secret_field,
112+ default_series_field,
113 is_default_field,
114 ),
115 }
116@@ -606,8 +612,7 @@
117 'fields': (
118 provider_field,
119 name_field,
120- admin_secret_field,
121- default_series_field,
122+ # SDC fields.
123 fields.StringField(
124 'sdc-user', required=True,
125 label='SDC user name',
126@@ -622,6 +627,7 @@
127 'sdc-url', choices=joyent_sdc_urls, label='SDC URL',
128 required=False, default=joyent_sdc_urls[0],
129 help='the region URL to use for SDC'),
130+ # Manta fields.
131 fields.StringField(
132 'manta-user', required=True,
133 label='manta user name',
134@@ -637,6 +643,7 @@
135 'manta-url', label='manta URL', required=False,
136 default='https://us-east.manta.joyent.com',
137 help='the region URL to use for manta'),
138+ # Optional fields.
139 fields.FilePathField(
140 'private-key-path', label='private key path', required=False,
141 default='~/.ssh/id_rsa',
142@@ -644,6 +651,8 @@
143 fields.StringField(
144 'algorithm', label='SSH algorithm', required=False,
145 default='rsa-sha256'),
146+ admin_secret_field,
147+ default_series_field,
148 is_default_field,
149 ),
150 }
151@@ -662,6 +671,7 @@
152 ),
153 'fields': (
154 provider_field,
155+ # Required fields.
156 name_field,
157 fields.StringField(
158 'maas-server', label='server address', required=True,
159@@ -674,6 +684,7 @@
160 'link to the account preferences is accessible from the '
161 'drop-down menu that appears when clicking your user '
162 'name at the top-right of the page.'),
163+ # Optional fields.
164 fields.FilePathField(
165 'authorized-keys-path', required=False,
166 label='SSH public key file',
167@@ -684,6 +695,49 @@
168 is_default_field,
169 ),
170 }
171+ env_type_db['manual'] = {
172+ 'label': 'Manual Provisioning',
173+ 'description': (
174+ 'Juju provides a feature called "Manual Provisioning" that '
175+ 'enables you to deploy Juju, and charms, to existing systems. '
176+ 'This is useful if you have groups of machines that you want to '
177+ "use for Juju but don't want to add the complexity of a new "
178+ 'OpenStack or MAAS setup. It is also useful as a means of '
179+ 'deploying workloads to VPS providers and other cheap hosting '
180+ 'options. Manual provisioning enables you to run Juju on systems '
181+ 'that have a supported operating system installed. You will need '
182+ 'to ensure that you have both SSH access and sudo rights. '
183+ 'See https://juju.ubuntu.com/docs/config-manual.html for more '
184+ 'details on the manual provider configuration, including its '
185+ 'caveats and limitattions.'
186+ ),
187+ 'fields': (
188+ provider_field,
189+ # Required fields.
190+ name_field,
191+ fields.StringField(
192+ 'bootstrap-host', label='bootstrap host', required=True,
193+ help='The host name/IP address of the machine where the '
194+ 'bootstrap machine agent will be started.'),
195+ # Optional fields.
196+ fields.StringField(
197+ 'bootstrap-user', label='bootstrap user', required=False,
198+ help='The user name to authenticate as when connecting to the '
199+ 'bootstrap machine. It defaults to the current user.'),
200+ fields.StringField(
201+ 'storage-listen-ip', label='storage listen IP', required=False,
202+ help="The IP address that the bootstrap machine's Juju "
203+ 'storage server will listen on. By default, storage will '
204+ 'be served on all network interfaces.'),
205+ fields.IntField(
206+ 'storage-port', min_value=1, max_value=65535,
207+ label='storage port', required=False, default=8040,
208+ help="The TCP port that the bootstrap machine's Juju storage "
209+ 'server will listen on.'),
210+ admin_secret_field,
211+ is_default_field,
212+ ),
213+ }
214 env_type_db['local'] = {
215 'label': 'local (LXC)',
216 'description': (
217@@ -695,25 +749,7 @@
218 'fields': (
219 provider_field,
220 name_field,
221- admin_secret_field,
222- default_series_field,
223- fields.StringField(
224- 'root-dir', label='root dir', required=False,
225- default='~/.juju/local',
226- help='the directory that is used for the storage files'),
227- fields.IntField(
228- 'storage-port', min_value=1, max_value=65535,
229- label='storage port', required=False, default=8040,
230- help='override if you have multiple local providers, '
231- 'or if the default port is used by another program'),
232- fields.IntField(
233- 'shared-storage-port', min_value=1, max_value=65535,
234- label='shared storage port', required=False, default=8041,
235- help='override if you have multiple local providers, '
236- 'or if the default port is used by another program'),
237- fields.StringField(
238- 'network-bridge', label='network bridge', required=False,
239- default='lxcbr0', help='the LXC bridge interface to use'),
240+ # LXC related fields.
241 fields.BoolField(
242 'lxc-clone', label='use lxc-clone', required=False,
243 help='Use lxc-clone to create the containers used as '
244@@ -725,6 +761,26 @@
245 "that there are some situations where aufs doesn't "
246 'entirely behave as intuitively as one might expect. '
247 'If not specified, aufs is not used.'),
248+ fields.StringField(
249+ 'root-dir', label='root dir', required=False,
250+ default='~/.juju/local',
251+ help='the directory that is used for the storage files'),
252+ fields.IntField(
253+ 'storage-port', min_value=1, max_value=65535,
254+ label='storage port', required=False, default=8040,
255+ help='override if you have multiple local providers, '
256+ 'or if the default port is used by another program'),
257+ fields.IntField(
258+ 'shared-storage-port', min_value=1, max_value=65535,
259+ label='shared storage port', required=False, default=8041,
260+ help='override if you have multiple local providers, '
261+ 'or if the default port is used by another program'),
262+ fields.StringField(
263+ 'network-bridge', label='network bridge', required=False,
264+ default='lxcbr0', help='the LXC bridge interface to use'),
265+ # Other optional fields.
266+ admin_secret_field,
267+ default_series_field,
268 is_default_field,
269 ),
270 }
271
272=== modified file 'quickstart/tests/cli/test_views.py'
273--- quickstart/tests/cli/test_views.py 2014-11-10 14:11:11 +0000
274+++ quickstart/tests/cli/test_views.py 2014-11-13 08:56:33 +0000
275@@ -706,14 +706,14 @@
276 expected_form_contents = [
277 ('provider type: ', 'local'),
278 ('environment name: ', 'lxc'),
279- ('admin secret: ', 'bones'),
280- ('default series: ', 'raring'),
281+ ('use lxc-clone', forms.MIXED),
282+ ('use lxc-clone-aufs', forms.MIXED),
283 ('root dir: ', ''),
284 ('storage port: ', '8888'),
285 ('shared storage port: ', ''),
286 ('network bridge: ', ''),
287- ('use lxc-clone', forms.MIXED),
288- ('use lxc-clone-aufs', forms.MIXED),
289+ ('admin secret: ', 'bones'),
290+ ('default series: ', 'raring'),
291 ('default', True),
292 ]
293 self.assertEqual(expected_form_contents, self.get_form_contents())
294
295=== modified file 'quickstart/tests/models/test_envs.py'
296--- quickstart/tests/models/test_envs.py 2014-11-11 13:21:57 +0000
297+++ quickstart/tests/models/test_envs.py 2014-11-13 08:56:33 +0000
298@@ -699,10 +699,9 @@
299 self.assertIn('local', self.env_type_db)
300 env_metadata = self.env_type_db['local']
301 expected = [
302- 'type', 'name', 'admin-secret', 'default-series', 'root-dir',
303+ 'type', 'name', 'lxc-clone', 'lxc-clone-aufs', 'root-dir',
304 'storage-port', 'shared-storage-port', 'network-bridge',
305- 'lxc-clone', 'lxc-clone-aufs',
306- 'is-default']
307+ 'admin-secret', 'default-series', 'is-default']
308 expected_required = ['type', 'name', 'is-default']
309 self.assert_fields(expected, env_metadata)
310 self.assert_required_fields(expected_required, env_metadata)
311@@ -712,8 +711,8 @@
312 self.assertIn('ec2', self.env_type_db)
313 env_metadata = self.env_type_db['ec2']
314 expected = [
315- 'type', 'name', 'admin-secret', 'default-series', 'access-key',
316- 'secret-key', 'control-bucket', 'region', 'is-default']
317+ 'type', 'name', 'access-key', 'secret-key', 'region',
318+ 'admin-secret', 'default-series', 'control-bucket', 'is-default']
319 expected_required = [
320 'type', 'name', 'access-key', 'secret-key',
321 'is-default']
322@@ -725,13 +724,13 @@
323 self.assertIn('openstack', self.env_type_db)
324 env_metadata = self.env_type_db['openstack']
325 expected = [
326- 'type', 'name', 'admin-secret', 'default-series',
327- 'use-floating-ip', 'control-bucket', 'auth-url', 'tenant-name',
328+ 'type', 'name', 'auth-url', 'tenant-name', 'use-floating-ip',
329 'region', 'auth-mode', 'username', 'password', 'access-key',
330- 'secret-key', 'is-default']
331+ 'secret-key', 'control-bucket', 'admin-secret', 'default-series',
332+ 'is-default']
333 expected_required = [
334- 'type', 'name', 'use-floating-ip',
335- 'auth-url', 'tenant-name', 'region', 'is-default']
336+ 'type', 'name', 'auth-url', 'tenant-name', 'use-floating-ip',
337+ 'region', 'is-default']
338 self.assert_fields(expected, env_metadata)
339 self.assert_required_fields(expected_required, env_metadata)
340
341@@ -740,13 +739,13 @@
342 self.assertIn('azure', self.env_type_db)
343 env_metadata = self.env_type_db['azure']
344 expected = [
345- 'type', 'name', 'admin-secret', 'default-series', 'location',
346- 'management-subscription-id', 'management-certificate-path',
347- 'storage-account-name', 'is-default']
348+ 'type', 'name', 'management-subscription-id',
349+ 'management-certificate-path', 'storage-account-name', 'location',
350+ 'admin-secret', 'default-series', 'is-default']
351 expected_required = [
352- 'type', 'name', 'location',
353- 'management-subscription-id', 'management-certificate-path',
354- 'storage-account-name', 'is-default']
355+ 'type', 'name', 'management-subscription-id',
356+ 'management-certificate-path', 'storage-account-name', 'location',
357+ 'is-default']
358 self.assert_fields(expected, env_metadata)
359 self.assert_required_fields(expected_required, env_metadata)
360
361@@ -755,10 +754,9 @@
362 self.assertIn('joyent', self.env_type_db)
363 env_metadata = self.env_type_db['joyent']
364 expected = [
365- 'type', 'name', 'admin-secret', 'default-series',
366- 'sdc-user', 'sdc-key-id', 'sdc-url',
367- 'manta-user', 'manta-key-id', 'manta-url',
368- 'private-key-path', 'algorithm', 'is-default']
369+ 'type', 'name', 'sdc-user', 'sdc-key-id', 'sdc-url', 'manta-user',
370+ 'manta-key-id', 'manta-url', 'private-key-path', 'algorithm',
371+ 'admin-secret', 'default-series', 'is-default']
372 expected_required = [
373 'type', 'name', 'sdc-user', 'sdc-key-id', 'manta-user',
374 'manta-key-id', 'is-default']
375@@ -778,6 +776,18 @@
376 self.assert_fields(expected, env_metadata)
377 self.assert_required_fields(expected_required, env_metadata)
378
379+ def test_manual_environment(self):
380+ # The manual environment metadata includes the expected fields.
381+ self.assertIn('manual', self.env_type_db)
382+ env_metadata = self.env_type_db['manual']
383+ expected = [
384+ 'type', 'name', 'bootstrap-host', 'bootstrap-user',
385+ 'storage-listen-ip', 'storage-port', 'admin-secret', 'is-default']
386+ expected_required = [
387+ 'type', 'name', 'bootstrap-host', 'is-default']
388+ self.assert_fields(expected, env_metadata)
389+ self.assert_required_fields(expected_required, env_metadata)
390+
391
392 class TestGetSupportedEnvTypes(unittest.TestCase):
393
394@@ -793,6 +803,7 @@
395 ('azure', 'Windows Azure'),
396 ('joyent', 'Joyent'),
397 ('maas', 'MAAS (bare metal)'),
398+ ('manual', 'Manual Provisioning'),
399 ('local', 'local (LXC)'),
400 ]
401 obtained_env_types = envs.get_supported_env_types(self.env_type_db)
402@@ -852,14 +863,14 @@
403 return [
404 ('type', 'local'),
405 ('name', 'lxc'),
406- ('admin-secret', 'Secret!'),
407- ('default-series', 'saucy'),
408+ ('lxc-clone', False),
409+ ('lxc-clone-aufs', False),
410 ('root-dir', '/my/juju/local/'),
411 ('storage-port', 4242),
412 ('shared-storage-port', 4747),
413 ('network-bridge', 'lxcbr1'),
414- ('lxc-clone', False),
415- ('lxc-clone-aufs', False),
416+ ('admin-secret', 'Secret!'),
417+ ('default-series', 'saucy'),
418 ('is-default', True),
419 ]
420
421@@ -874,14 +885,14 @@
422 expected = [
423 ('type', 'local'),
424 ('name', 'lxc'),
425- ('admin-secret', None),
426- ('default-series', None),
427+ ('lxc-clone', None),
428+ ('lxc-clone-aufs', None),
429 ('root-dir', None),
430 ('storage-port', None),
431 ('shared-storage-port', None),
432 ('network-bridge', None),
433- ('lxc-clone', None),
434- ('lxc-clone-aufs', None),
435+ ('admin-secret', None),
436+ ('default-series', None),
437 ('is-default', None),
438 ]
439 env_data = {'type': 'local', 'name': 'lxc'}

Subscribers

People subscribed via source and target branches