-
-
Notifications
You must be signed in to change notification settings - Fork 6
Expand file tree
/
Copy pathupload.ex
More file actions
197 lines (145 loc) · 5.08 KB
/
Copy pathupload.ex
File metadata and controls
197 lines (145 loc) · 5.08 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
# SPDX-FileCopyrightText: 2020 Frank Hunleth
# SPDX-FileCopyrightText: 2022 Jon Carstens
# SPDX-FileCopyrightText: 2024 Benjamin Milde
# SPDX-FileCopyrightText: 2024 Jon Ringle
#
# SPDX-License-Identifier: Apache-2.0
#
defmodule Mix.Tasks.Upload do
use Mix.Task
@shortdoc "Uploads firmware to a Nerves device over SSH"
@moduledoc """
Upgrade the firmware on a Nerves device using SSH.
By default, `mix upload` reads the firmware built by the current `MIX_ENV`
and `MIX_TARGET` settings, and sends it to `nerves.local`. Pass in a another
hostname to send the firmware elsewhere.
NOTE: This implementation cannot ask for passphrases, and therefore, cannot
connect to devices protected by username/passwords or decrypt
password-protected private keys. One workaround is to use the `ssh-agent` to
pass credentials.
## Command line options
* `--firmware` - The path to a fw file
* `--port` - An alternative TCP port to use for the upload (defaults to 22)
* `--task` - The fwup task to run on the device (defaults to "upgrade").
Use this to run alternative tasks like "complete" or custom tasks defined
in your firmware.
## Examples
Upgrade a Raspberry Pi Zero at `nerves.local`:
MIX_TARGET=rpi0 mix upload nerves.local
Upgrade `192.168.1.120` and explicitly pass the `.fw` file:
mix upload 192.168.1.120 --firmware _build/rpi0_prod/nerves/images/app.fw
Run the complete task instead of upgrade:
MIX_TARGET=rpi0 mix upload nerves.local --task complete
"""
@switches [
firmware: :string,
port: :integer,
task: :string
]
@doc false
@spec run([String.t()]) :: :ok
def run(argv) do
{opts, args, unknown} = OptionParser.parse(argv, strict: @switches)
if unknown != [] do
[{param, _} | _] = unknown
Mix.raise("unknown parameter passed to mix upload: #{param}")
end
ip =
case args do
[address] -> address
[] -> "nerves.local"
_other -> Mix.raise(target_ip_address_or_name_msg())
end
check_requirements!()
port = opts[:port] || 22
validate_port!(port)
task = opts[:task]
task_env = if task, do: "FWUP_TASK=#{task} ", else: ""
send_env_opt = if task, do: "-o SendEnv=FWUP_TASK ", else: ""
firmware_path = firmware(opts)
Mix.shell().info("""
Path: #{firmware_path}
#{maybe_print_firmware_uuid(firmware_path)}
#{maybe_print_task(task)}Uploading to #{ip}:#{port}...
""")
# LD_LIBRARY_PATH is unset to avoid errors with host ssl (see commit 9b1df471)
{_, status} =
InteractiveCmd.shell(
"#{task_env}cat #{shell_quote(firmware_path)} | ssh #{send_env_opt}-p #{port} -s -- #{shell_quote(ip)} fwup",
env: [{"LD_LIBRARY_PATH", false}]
)
if status != 0 do
Mix.raise("""
Failed to upgrade the device.
If this persists and it's not a networking or device issue, please
try using the `upload.sh` script generated by `mix firmware.gen.script`.
""")
end
:ok
end
defp firmware(opts) do
path = opts[:firmware] || default_firmware()
absolute_path = Path.expand(path)
if not File.exists?(absolute_path) do
Mix.raise("""
The firmware file does not exist.
Path:
#{absolute_path}
Run `mix firmware` to build it or check the path.
""")
end
absolute_path
end
defp default_firmware() do
if Mix.target() == :host do
Mix.raise("""
You must call mix with a target set or pass the firmware's path.
Examples:
$ MIX_TARGET=rpi0 mix upload nerves.local
or
$ mix upload nerves.local --firmware _build/rpi0_prod/nerves/images/app.fw
""")
end
build_path = Mix.Project.build_path()
app = Mix.Project.config()[:app]
Path.join([build_path, "nerves", "images", "#{app}.fw"])
end
defp check_requirements!() do
check_ssh!()
with {:error, reason} <- InteractiveCmd.check_requirements() do
Mix.raise(reason)
end
:ok
end
defp check_ssh!() do
if System.find_executable("ssh") == nil do
Mix.raise("""
Cannot find 'ssh'. Check that it exists in your path
""")
end
end
defp validate_port!(port) when is_integer(port) and port > 0 and port <= 65535, do: :ok
defp validate_port!(port) do
Mix.raise("Invalid port: #{inspect(port)}. Port must be an integer between 1 and 65535.")
end
defp target_ip_address_or_name_msg() do
~S"""
mix upload expects a target IP address or hostname
Example:
If the device is reachable using `nerves-1234.local`, try:
`mix upload nerves-1234.local`
"""
end
defp maybe_print_firmware_uuid(fw_path) do
fwup = System.find_executable("fwup")
{uuid, 0} = System.cmd(fwup, ["-m", "--metadata-key", "meta-uuid", "-i", fw_path])
"UUID: #{uuid}\n"
catch
# fwup may not be on the host or something else failed, but continue
# on as normal by returning an empty line
_, _ -> ""
end
defp maybe_print_task(nil), do: ""
defp maybe_print_task(task), do: "Task: #{task}\n"
defp shell_quote(str), do: "'" <> String.replace(str, "'", "'\"'\"'") <> "'"
end