@@ -9,7 +9,7 @@ defmodule Bootleg.Config do
9
9
defmacro __using__ ( _ ) do
10
10
quote do
11
11
import Bootleg.Config , only: [ role: 2 , role: 3 , config: 2 , config: 0 , before_task: 2 ,
12
- after_task: 2 , invoke: 1 , task: 2 , remote: 1 , remote: 2 , load: 1 ]
12
+ after_task: 2 , invoke: 1 , task: 2 , remote: 1 , remote: 2 , remote: 3 , load: 1 , upload: 3 ]
13
13
end
14
14
end
15
15
@@ -22,7 +22,7 @@ defmodule Bootleg.Config do
22
22
23
23
`name` is the name of the role, and is globally unique. Calling `role/3` multiple times with
24
24
the same name will result in the host lists being merged. If the same host shows up mutliple
25
- times, it will have its `options` merged.
25
+ times, it will have its `options` merged. The name `:all` is reserved and cannot be used here.
26
26
27
27
`hosts` can be a single hostname, or a `List` of hostnames.
28
28
@@ -41,6 +41,9 @@ defmodule Bootleg.Config do
41
41
"""
42
42
defmacro role ( name , hosts , options \\ [ ] ) do
43
43
# user is in the role options for scm
44
+ if name == :all do
45
+ raise ArgumentError , ":all is reserved by bootleg and refers to all defined roles."
46
+ end
44
47
user = Keyword . get ( options , :user , System . get_env ( "USER" ) )
45
48
ssh_options = Enum . filter ( options , & Enum . member? ( SSH . supported_options , elem ( & 1 , 0 ) ) == true )
46
49
role_options =
@@ -325,11 +328,28 @@ defmodule Bootleg.Config do
325
328
end
326
329
327
330
defmacro remote ( role , do: { :__block__ , _ , lines } ) do
328
- quote do: remote ( unquote ( role ) , unquote ( lines ) )
331
+ quote do: remote ( unquote ( role ) , [ ] , unquote ( lines ) )
329
332
end
330
333
331
334
defmacro remote ( role , do: lines ) do
332
- quote do: remote ( unquote ( role ) , unquote ( lines ) )
335
+ quote do: remote ( unquote ( role ) , [ ] , unquote ( lines ) )
336
+ end
337
+
338
+ @ doc """
339
+ Executes commands on all remote hosts within a role.
340
+
341
+ This is equivalent to calling `remote/3` with a `filter` of `[]`.
342
+ """
343
+ defmacro remote ( role , lines ) do
344
+ quote do: remote ( unquote ( role ) , [ ] , unquote ( lines ) )
345
+ end
346
+
347
+ defmacro remote ( role , filter , do: { :__block__ , _ , lines } ) do
348
+ quote do: remote ( unquote ( role ) , unquote ( filter ) , unquote ( lines ) )
349
+ end
350
+
351
+ defmacro remote ( role , filter , do: lines ) do
352
+ quote do: remote ( unquote ( role ) , unquote ( filter ) , unquote ( lines ) )
333
353
end
334
354
335
355
@ doc """
@@ -343,6 +363,10 @@ defmodule Bootleg.Config do
343
363
used as a command. Each command will be simulataneously executed on all hosts in the role. Once
344
364
all hosts have finished executing the command, the next command in the list will be sent.
345
365
366
+ `filter` is an optional `Keyword` list of host options to filter with. Any host whose options match
367
+ the filter will be included in the remote execution. A host matches if it has all of the filtering
368
+ options defined and the values match (via `==/2`) the filter.
369
+
346
370
`role` can be a single role, a list of roles, or the special role `:all` (all roles). If the same host
347
371
exists in multiple roles, the commands will be run once for each role where the host shows up. In the
348
372
case of multiple roles, each role is processed sequentially.
@@ -367,18 +391,22 @@ defmodule Bootleg.Config do
367
391
368
392
# runs for hosts found in :build first, then for hosts in :app
369
393
remote [:build, :app], do: "hostname"
394
+
395
+ # only runs on `host1.example.com`
396
+ role :build, "host2.example.com"
397
+ role :build, "host1.example.com", primary: true, another_attr: :cat
398
+
399
+ remote :build, primary: true do
400
+ "hostname"
401
+ end
370
402
```
371
403
"""
372
- defmacro remote ( role , lines ) do
373
- roles = if role == :all do
374
- quote do: Keyword . keys ( Bootleg.Config.Agent . get ( :roles ) )
375
- else
376
- quote do: List . wrap ( unquote ( role ) )
377
- end
404
+ defmacro remote ( role , filter , lines ) do
405
+ roles = unpack_role ( role )
378
406
quote bind_quoted: binding ( ) do
379
407
Enum . reduce ( roles , [ ] , fn role , outputs ->
380
408
role
381
- |> SSH . init
409
+ |> SSH . init ( [ ] , filter )
382
410
|> SSH . run! ( lines )
383
411
|> SSH . merge_run_results ( outputs )
384
412
end )
@@ -401,6 +429,56 @@ defmodule Bootleg.Config do
401
429
end
402
430
end
403
431
432
+ @ doc """
433
+ Uploads a local file to remote hosts.
434
+
435
+ Uploading works much like `remote/3`, but instead of transferring shell commands over SSH,
436
+ it transfers files via SCP. The remote host does need to support SCP, which should be provided
437
+ by most SSH implementations automatically.
438
+
439
+ `role` can either be a single role name, a list of roles, or a list of roles and filter
440
+ attributes. The special `:all` role is also supported. See `remote/3` for details.
441
+
442
+ `local_path` can either be a file or directory found on the local machine. If its a directory,
443
+ the entire directory will be recursively copied to the remote hosts. Relative paths are resolved
444
+ relative to the root of the local project.
445
+
446
+ `remote_path` is the file or directory where the transfered files should be placed. The semantics
447
+ of how `remote_path` is treated vary depending on what `local_path` refers to. If `local_path` points
448
+ to a file, `remote_path` is treated as a file unless it's `.` or ends in `/`, in which case it's
449
+ treated as a directory and the filename of the local file will be used. If `local_path` is a directory,
450
+ `remote_path` is treated as a directory as well. Relative paths are resolved relative to the projects
451
+ remote `workspace`. Missing directories are not implicilty created.
452
+
453
+ The files on the remote server are created using the authenticating user's `uid`/`gid` and `umask`.
454
+
455
+ ```
456
+ use Bootleg.Config
457
+
458
+ # copies ./my_file to ./new_name on the remote host
459
+ upload :app, "my_file", "new_name"
460
+
461
+ # copies ./my_file to ./a_dir/my_file on the remote host. ./a_dir must already exist
462
+ upload :app, "my_file", "a_dir/"
463
+
464
+ # recursively copies ./some_dir to ./new_dir on the remote host. ./new_dir will be created if missing
465
+ upload :app, "some_dir", "new_dir"
466
+
467
+ # copies ./my_file to /tmp/foo on the remote host
468
+ upload :app, "my_file", "/tmp/foo"
469
+ """
470
+ defmacro upload ( role , local_path , remote_path ) do
471
+ { roles , filters } = split_roles_and_filters ( role )
472
+ roles = unpack_role ( roles )
473
+ quote bind_quoted: binding ( ) do
474
+ Enum . each ( roles , fn role ->
475
+ role
476
+ |> SSH . init ( [ ] , filters )
477
+ |> SSH . upload ( local_path , remote_path )
478
+ end )
479
+ end
480
+ end
481
+
404
482
@ doc false
405
483
@ spec get_config ( atom , any ) :: any
406
484
def get_config ( key , default \\ nil ) do
@@ -418,4 +496,23 @@ defmodule Bootleg.Config do
418
496
def version do
419
497
get_config ( :version , Project . config [ :version ] )
420
498
end
499
+
500
+ @ doc false
501
+ @ spec split_roles_and_filters ( atom | keyword ) :: { [ atom ] , keyword }
502
+ defp split_roles_and_filters ( role ) do
503
+ role
504
+ |> List . wrap
505
+ |> Enum . split_while ( fn term -> ! is_tuple ( term ) end )
506
+ end
507
+
508
+ @ doc false
509
+ @ spec unpack_role ( atom | keyword ) :: tuple
510
+ defp unpack_role ( role ) do
511
+ wrapped_role = List . wrap ( role )
512
+ if Enum . any? ( wrapped_role , fn role -> role == :all end ) do
513
+ quote do: Keyword . keys ( Bootleg.Config.Agent . get ( :roles ) )
514
+ else
515
+ quote do: unquote ( wrapped_role )
516
+ end
517
+ end
421
518
end
0 commit comments