Merge ~knitzsche/snappy-hwe-snaps/+git/wifi-connect:defaults into ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master

Proposed by Kyle Nitzsche
Status: Merged
Approved by: Roberto Mier Escandon
Approved revision: 174a30c8d126a5c65dd79770c04e9561a52b0c16
Merged at revision: d8fe23b5e46dd61a32c597d146aa434feb2958cd
Proposed branch: ~knitzsche/snappy-hwe-snaps/+git/wifi-connect:defaults
Merge into: ~snappy-hwe-team/snappy-hwe-snaps/+git/wifi-connect:master
Diff against target: 941 lines (+700/-38)
15 files modified
README.md (+1/-1)
daemon/daemon.go (+66/-6)
daemon/daemon_test.go (+121/-20)
docs/faq.md (+10/-0)
docs/index.md (+24/-0)
docs/installation.md (+121/-0)
docs/integrate.md (+70/-0)
docs/metadata.yaml (+12/-0)
hooks/configure.go (+111/-0)
hooks/configure_test.go (+110/-0)
service/service.go (+14/-6)
snap/snapcraft.yaml (+8/-3)
static/tests/pre-config0.json (+7/-0)
static/tests/pre-config1.json (+5/-0)
wifiap/wifiap.go (+20/-2)
Reviewer Review Type Date Requested Status
Roberto Mier Escandon (community) Approve
Konrad Zapałowicz (community) code Approve
System Enablement Bot continuous-integration Approve
Review via email: mp+326172@code.launchpad.net

Description of the change

Add configure hook to port certain snap wifi-connect keys into SNAP_COMMON/pre-config.json.

Deamon (service/service.go and daemon pkg) uses SetDefaults() to use the json to take the appropriate actions. [1]

docs/ added this, including integrate.md to doc the exactl preconfiguration stuff for system intergrators.

wifiap/wifiap.go: added Wifiaper interface to make wifiap unit-testable in SetDefaults()

Enhanced daemon_test.go TestSetDefaults to handle various preconfigs via static/tests/pre-config*.json

[1] https://docs.google.com/document/d/1QaKcHikGi6ttuKLgT2Id42-hQXRng1B68ucxEK-2zws/edit#heading=h.ikzb28sk2cu3

To post a comment you must log in.
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Sheila Miguez (codersquid) wrote :

minor inline comment

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Sheila Miguez (codersquid) wrote :

Ignore my inline comment, I missed where the user gets feedback way below.

Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Comments inline.

review: Needs Fixing (code)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

I've left a pair of inline comments.

review: Needs Fixing
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

After discussion with Konrad, I'll leave the markdown as is since I consistently use code blocks.

After discussion with Roberto, I'll leave the name of "Wifiaper" interface as is for consistency with golang interface naming conventions.

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

replied to all other comments inline, mostly saying "yes you are right", one way or another ;)

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

> After discussion with Konrad, I'll leave the markdown as is since I
> consistently use code blocks.
>
> After discussion with Roberto, I'll leave the name of "Wifiaper" interface as
> is for consistency with golang interface naming conventions.

A note here about the name of the interface:

https://golang.org/doc/effective_go.html#interface-names says "one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun". But nothing specific says about interfaces having more than one method. So, golang naming conventions nothing say about this case. (or at least I haven't found anything)

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

thanks for pointing this out Roberto

On 06/26/2017 07:09 AM, Roberto Mier Escandón  wrote:
>> After discussion with Konrad, I'll leave the markdown as is since I
>> consistently use code blocks.
>>
>> After discussion with Roberto, I'll leave the name of "Wifiaper" interface as
>> is for consistency with golang interface naming conventions.
>
> A note here about the name of the interface:
>
> https://golang.org/doc/effective_go.html#interface-names says "one-method interfaces are named by the method name plus an -er suffix or similar modification to construct an agent noun". But nothing specific says about interfaces having more than one method. So, golang naming conventions nothing say about this case. (or at least I haven't found anything)
>

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

READY for review - thanks

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

Some inline comments

review: Needs Fixing
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Roberto, pls see my responses, a couple small code changes coming.

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

rereplied

Revision history for this message
Kyle Nitzsche (knitzsche) wrote :
Download full text (7.2 KiB)

On 06/27/2017 09:08 AM, Roberto Mier Escandón  wrote:
> rereplied
>
> Diff comments:
>
>> diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
>> index c6242cd..a2bfca3 100644
>> --- a/daemon/daemon_test.go
>> +++ b/daemon/daemon_test.go
>> @@ -94,8 +98,76 @@ func TestManualMode(t *testing.T) {
>> }
>> }
>>
>> +type mockWifiap struct{}
>> +
>> +//var wifiapClient wifiapInterface
>
> commented on purpose?
>
>> +
>> +func (mock *mockWifiap) Do(req *http.Request) (*http.Response, error) {
>> + fmt.Println("==== MY do called")
>> + url := req.URL.String()
>> + if url != "http://unix/v1/configuration" {
>> + return nil, fmt.Errorf("Not valid request URL: %v", url)
>> + }
>> +
>> + if req.Method != "GET" {
>> + return nil, fmt.Errorf("Method is not valid. Expected GET, got %v", req.Method)
>> + }
>> +
>> + rawBody := `{"result":{
>> + "debug":false,
>> + "dhcp.lease-time": "12h",
>> + "dhcp.range-start": "10.0.60.2",
>> + "dhcp.range-stop": "10.0.60.199",
>> + "disabled": true,
>> + "share.disabled": false,
>> + "share-network-interface": "tun0",
>> + "wifi-address": "10.0.60.1",
>> + "wifi.channel": "6",
>> + "wifi.hostapd-driver": "nl80211",
>> + "wifi.interface": "wlan0",
>> + "wifi.interface-mode": "direct",
>> + "wifi.netmask": "255.255.255.0",
>> + "wifi.operation-mode": "g",
>> + "wifi.security": "
>> + "wifi.security-passphrase": "passphrase123",
>> + "wifi.ssid": "AP"},"status":"OK","status-code":200,""sync"}`
>> +
>> + response := http.Response{
>> + StatusCode: 200,
>> + Status: "200 OK",
>> + Body: ioutil.NopCloser(strings.NewReader(rawBody)),
>> + }
>> +
>> + return &response, nil
>> +}
>> +
>> +func (mock *mockWifiap) Show() (map[string]interface{}, error) {
>> + wifiAp := make(map[string]interface{})
>> + wifiAp["wifi.security-passphrase"] = "randompassphrase"
>> + return wifiAp, nil
>> +}
>> +
>> +func (mock *mockWifiap) Enabled() (bool, error) {
>> + return true, nil
>> +}
>> +
>> +func (mock *mockWifiap) Enable() error {
>> + return nil
>> +}
>> +func (mock *mockWifiap) Disable() error {
>> + return nil
>> +}
>> +func (mock *mockWifiap) SetSsid(s string) error {
>> + return nil
>> +}
>> +
>> +func (mock *mockWifiap) SetPassphrase(p string) error {
>> + return nil
>> +}
>> +
>> func TestSetDefaults(t *testing.T) {
>> client := GetClient()
>> + PreConfigFile = "../static/tests/pre-config0.json"
>> hfp := "/tmp/hash"
>> if _, err := os.Stat(hfp); err == nil {
>> err = os.Remove(hfp)
>> diff --git a/hooks/configure.go b/hooks/configure.go
>> new file mode 100644
>> index 0000000..c674cca
>> --- /dev/null
>> +++ b/hooks/configure.go
>> @@ -0,0 +1,111 @@
>> +// -*- Mode: Go; indent-tabs-mode: t -*-
>> +
>> +/*
>> + * Copyright (C) 2017 Canonical Ltd
>> + *
>> + * This program is free software: you can redistribute it and/or modify
>> + * it under the terms of the GNU General Public License version 3 as
>> + * published by the Free Software Foundation.
>> + *
>> + * This program is distributed in the hope that it will be useful,
>> + * but WITHOUT ANY WARRANTY; without even the implied warranty of
>> + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
>> + ...

Read more...

Revision history for this message
Roberto Mier Escandon (rmescandon) wrote :

1) can we agree to disagree on this or do you feel strongly?

+1. This is not crucial.

2) That's not quite the reason I tried to explain previously. Should we discuss in hang out so we don't keep going round in circles on this point?

Np. This way also is valid and are the names I'm using in config-ui pr. +1 also

Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Some more comments, this SetDefaults thing is an interesting exercise! I'm half-way through, will finish later today.

review: Needs Fixing (code)
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

Finished

review: Needs Fixing (code)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Kyle Nitzsche (knitzsche) wrote :

Ready for re-review - pls see comments above (although lp formatting seems to have injected numerous extra "*"s ;)

Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
System Enablement Bot (system-enablement-ci-bot) wrote :
review: Approve (continuous-integration)
Revision history for this message
Konrad Zapałowicz (kzapalowicz) wrote :

ack

review: Approve (code)
Revision history for this message
Roberto Mier Escandon (rmescandon) :
review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1diff --git a/README.md b/README.md
2index d100e2f..1759797 100644
3--- a/README.md
4+++ b/README.md
5@@ -44,7 +44,7 @@ snap connect wifi-connect:network-manager network-manager:service
6 Note: wifi-ap and network-manager interfaces auto-connect.
7 Note: Currently content share interface requires a reboot after connection, as described below.
8
9-# Set NetWorkManager to control all networking
10+# Set NetworkManager to control all networking
11
12 Note: This is a temporary manual step before network-manager snap provides a config option for this.
13
14diff --git a/daemon/daemon.go b/daemon/daemon.go
15index 04ff59e..a888969 100644
16--- a/daemon/daemon.go
17+++ b/daemon/daemon.go
18@@ -18,8 +18,11 @@
19 package daemon
20
21 import (
22+ "encoding/json"
23 "fmt"
24+ "io/ioutil"
25 "os"
26+ "path/filepath"
27
28 "launchpad.net/wifi-connect/avahi"
29 "launchpad.net/wifi-connect/server"
30@@ -40,6 +43,19 @@ var waitFlagPath string
31 var previousState = STARTING
32 var state = STARTING
33
34+// PreConfigFile is the path to the file that stores the hash of the portals password
35+var PreConfigFile = filepath.Join(os.Getenv("SNAP_COMMON"), "pre-config.json")
36+
37+// PreConfig is the struct representing a configuration
38+type PreConfig struct {
39+ Passphrase string `json:"wifi.security-passphrase,omitempty"`
40+ Ssid string `json:"wifi.ssid,omitempty"`
41+ Interface string `json:"wifi.interface,omitempty"`
42+ Password string `json:"portal.password,omitempty"`
43+ NoOperational bool `json:"portal.no-operational,omitempty"` //whether to show the operational portal
44+ NoResetCreds bool `json:"portal.no-reset-creds,omitempty"` //whether user must reset passphrase and password on first use of mgmt portal
45+}
46+
47 // Client is the base type for both testing and runtime
48 type Client struct {
49 }
50@@ -189,11 +205,55 @@ func (c *Client) OperationalServerDown() {
51 }
52 }
53
54-// SetDefaults sets defaults if not yet set. Currently the hash
55-// for the portals password is set.
56-// TODO: set default password based on MAC addr or Serial number
57-func (c *Client) SetDefaults() {
58- if _, err := os.Stat(utils.HashFile); os.IsNotExist(err) {
59- utils.HashIt("wifi-connect")
60+// LoadPreConfig returns a PreConfig based on the pre-config.json, if present, and an error to indicate
61+// possible json unmarshal failure
62+func LoadPreConfig() (*PreConfig, error) {
63+ config := &PreConfig{}
64+ content, err := ioutil.ReadFile(PreConfigFile)
65+ if err == nil {
66+ fmt.Println("== wifi-connect/daemon: preconfiguration file found")
67+ }
68+ err = json.Unmarshal(content, config)
69+ if err != nil {
70+ return config, err
71+ }
72+ return config, nil
73+}
74+
75+// SetDefaults creates the run time configuration based on wifi-ap and the pre-config.json
76+// configuration file, if any. The configuration is returned with an error. PreConfig.PreConfigfile
77+// indicates whether a pre-config file exists.
78+func (c *Client) SetDefaults(cw wifiap.Operations, config *PreConfig) error {
79+ if err != nil {
80+ fmt.Println("== wifi-connect/daemon/SetDefaults: preconfig unmarshall errorr:", err)
81+ }
82+ ap, errShow := cw.Show()
83+ if errShow != nil {
84+ fmt.Println("== wifi-connect/daemon/SetDefaults: wifi-ap.Show err:", errShow)
85+ }
86+ if ap["wifi.security-passphrase"] != config.Passphrase {
87+ if len(config.Passphrase) > 0 {
88+ err = cw.SetPassphrase(config.Passphrase)
89+ fmt.Println("== wifi-connect/SetDefaults wifi-ap passphrase being set")
90+ if err != nil {
91+ fmt.Println("== wifi-connect/daemon/SetDefaults: passphrase err:", err)
92+ return err
93+ }
94+ }
95+ }
96+ if len(config.Password) > 0 {
97+ fmt.Println("== wifi-connect/SetDefaults portal password being set")
98+ _, err = utils.HashIt(config.Password)
99+ if err != nil {
100+ fmt.Println("== wifi-connect/daemon/SetDefaults: password err:", err)
101+ return err
102+ }
103+ }
104+ if config.NoOperational {
105+ fmt.Println("== wifi-connect/SetDefaults: operational portal is now disabled")
106+ }
107+ if config.NoResetCreds {
108+ fmt.Println("== wifi-connect/SetDefaults: reset creds requirement is now disabled")
109 }
110+ return nil
111 }
112diff --git a/daemon/daemon_test.go b/daemon/daemon_test.go
113index c6242cd..177c26b 100644
114--- a/daemon/daemon_test.go
115+++ b/daemon/daemon_test.go
116@@ -18,7 +18,11 @@
117 package daemon
118
119 import (
120+ "fmt"
121+ "io/ioutil"
122+ "net/http"
123 "os"
124+ "strings"
125 "testing"
126
127 "launchpad.net/wifi-connect/utils"
128@@ -94,8 +98,92 @@ func TestManualMode(t *testing.T) {
129 }
130 }
131
132+type mockWifiap struct{}
133+
134+func (mock *mockWifiap) Do(req *http.Request) (*http.Response, error) {
135+ fmt.Println("==== MY do called")
136+ url := req.URL.String()
137+ if url != "http://unix/v1/configuration" {
138+ return nil, fmt.Errorf("Not valid request URL: %v", url)
139+ }
140+
141+ if req.Method != "GET" {
142+ return nil, fmt.Errorf("Method is not valid. Expected GET, got %v", req.Method)
143+ }
144+
145+ rawBody := `{"result":{
146+ "debug":false,
147+ "dhcp.lease-time": "12h",
148+ "dhcp.range-start": "10.0.60.2",
149+ "dhcp.range-stop": "10.0.60.199",
150+ "disabled": true,
151+ "share.disabled": false,
152+ "share-network-interface": "tun0",
153+ "wifi-address": "10.0.60.1",
154+ "wifi.channel": "6",
155+ "wifi.hostapd-driver": "nl80211",
156+ "wifi.interface": "wlan0",
157+ "wifi.interface-mode": "direct",
158+ "wifi.netmask": "255.255.255.0",
159+ "wifi.operation-mode": "g",
160+ "wifi.security": "
161+ "wifi.security-passphrase": "passphrase123",
162+ "wifi.ssid": "AP"},"status":"OK","status-code":200,""sync"}`
163+
164+ response := http.Response{
165+ StatusCode: 200,
166+ Status: "200 OK",
167+ Body: ioutil.NopCloser(strings.NewReader(rawBody)),
168+ }
169+
170+ return &response, nil
171+}
172+
173+func (mock *mockWifiap) Show() (map[string]interface{}, error) {
174+ wifiAp := make(map[string]interface{})
175+ wifiAp["wifi.security-passphrase"] = "randompassphrase"
176+ return wifiAp, nil
177+}
178+
179+func (mock *mockWifiap) Enabled() (bool, error) {
180+ return true, nil
181+}
182+
183+func (mock *mockWifiap) Enable() error {
184+ return nil
185+}
186+func (mock *mockWifiap) Disable() error {
187+ return nil
188+}
189+func (mock *mockWifiap) SetSsid(s string) error {
190+ return nil
191+}
192+
193+func (mock *mockWifiap) SetPassphrase(p string) error {
194+ return nil
195+}
196+
197+func TestLoadPreConfig(t *testing.T) {
198+ PreConfigFile = "../static/tests/pre-config0.json"
199+ config, err := LoadPreConfig()
200+ if err != nil {
201+ t.Errorf("Unexpected error using LoadPreConfig: %s", err)
202+ }
203+ if config.Passphrase != "abcdefghijklmnop" {
204+ t.Errorf("Passphrase of %s expected but got %s:", "abcdefghijklmnop", config.Passphrase)
205+ }
206+ if !config.NoOperational {
207+ t.Errorf("portal.no-operational was set to true but the loaded config is %t", config.NoOperational)
208+ }
209+ if !config.NoResetCreds {
210+ t.Errorf("portal.no-reset-creds was set to true but the loaded config is %t", config.NoResetCreds)
211+ }
212+
213+}
214+
215 func TestSetDefaults(t *testing.T) {
216 client := GetClient()
217+ PreConfigFile = "../static/tests/pre-config0.json"
218 hfp := "/tmp/hash"
219 if _, err := os.Stat(hfp); err == nil {
220 err = os.Remove(hfp)
221@@ -103,35 +191,48 @@ func TestSetDefaults(t *testing.T) {
222 t.Errorf("Could not remove previous file version")
223 }
224 }
225+ config, _ := LoadPreConfig()
226 utils.SetHashFile(hfp)
227- client.SetDefaults()
228- _, err := os.Stat(utils.HashFile)
229+ client.SetDefaults(&mockWifiap{}, config)
230+ expectedPassphrase := "abcdefghijklmnop"
231+ expectedPassword := "qwerzxcv"
232+ if config.Passphrase != expectedPassphrase {
233+ t.Errorf("SetDefaults: Preconfig passphrase should be %s but is %s", expectedPassphrase, config.Passphrase)
234+ }
235 if os.IsNotExist(err) {
236 t.Errorf("SetDefaults should have created %s but did not", hfp)
237 }
238- res, _ := utils.MatchingHash("wifi-connect")
239+ res, _ := utils.MatchingHash(expectedPassword)
240 if !res {
241- t.Errorf("SetDefaults password match did not match")
242+ t.Errorf("SetDefaults: Preconfig password hash did not match actual")
243+ }
244+ if !config.NoOperational {
245+ t.Errorf("SetDefaults: Preconfig portal.no-operational should be true (set) but is %t", config.NoOperational)
246+ }
247+ if !config.NoResetCreds {
248+ t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be true (set) but is %t", config.NoResetCreds)
249 }
250-}
251
252-func TestSetDefaultsAlreadyExistsHashFile(t *testing.T) {
253- client := GetClient()
254- hfp := "/tmp/hash"
255- // create file if not exists
256- if _, err := os.Stat(utils.HashFile); os.IsNotExist(err) {
257- if _, err = os.OpenFile(hfp, os.O_CREATE, 0666); err != nil {
258- t.Errorf("Error creating %v file", hfp)
259+ if _, err := os.Stat(hfp); err == nil {
260+ err = os.Remove(hfp)
261+ if err != nil {
262+ t.Errorf("Could not remove previous file version")
263 }
264 }
265- utils.SetHashFile(hfp)
266- client.SetDefaults()
267- _, err := os.Stat(utils.HashFile)
268- if os.IsNotExist(err) {
269- t.Errorf("SetDefaults should have created %s but did not", hfp)
270+ PreConfigFile = "../static/tests/pre-config1.json"
271+ config, _ = LoadPreConfig()
272+ client.SetDefaults(&mockWifiap{}, config)
273+ if len(config.Passphrase) > 0 {
274+ t.Errorf("SetDefaults: Preconfig passphrase was not set but is %s", config.Passphrase)
275 }
276- res, _ := utils.MatchingHash("wifi-connect")
277- if !res {
278- t.Errorf("SetDefaults password match did not match")
279+ res2, _ := utils.MatchingHash(expectedPassword)
280+ if res2 {
281+ t.Errorf("SetDefaults: Preconfig password was not set, but the hash matched")
282+ }
283+ if config.NoOperational {
284+ t.Errorf("SetDefaults: Preconfig portal.no-operational should be false (unset) but is %t", config.NoOperational)
285+ }
286+ if config.NoResetCreds {
287+ t.Errorf("SetDefaults: Preconfig portal.no-reset-creds should be false (unnset) but is %t", config.NoResetCreds)
288 }
289 }
290diff --git a/docs/faq.md b/docs/faq.md
291new file mode 100644
292index 0000000..ec8f87e
293--- /dev/null
294+++ b/docs/faq.md
295@@ -0,0 +1,10 @@
296+---
297+title: "Frequently asked questions"
298+table_of_contents: True
299+---
300+
301+# Restarting the wifi-connect daemon
302+
303+```bash
304+sudo systemctl restart snap.wifi-connect.daemon.service
305+```
306diff --git a/docs/index.md b/docs/index.md
307new file mode 100644
308index 0000000..679b180
309--- /dev/null
310+++ b/docs/index.md
311@@ -0,0 +1,24 @@
312+---
313+title: "Overview"
314+table_of_contents: True
315+---
316+
317+# Overview
318+
319+The wifi-connect snap allows you to connect your device to an external Wi-Fi access point. It does this by putting up its own Wi-Fi AP and web page. You join the AP and the web page lists external APs, which you can then select and join.
320+
321+Wifi-connect is appropriate for simple use cases where there is no other control of networking. Wifi-connect has a daemon that takes over networking and controls device state automatically:
322+
323+ * When there is no external AP connection, wifi-connect starts its own AP and the Management web page (which allows you to select and join external WiFI APs)
324+ * When there is an external AP connection, wifi-connect ensures its own AP is down and puts up the Operational web page (which allows you to disconnect from the external AP)
325+
326+Wifi-connect uses two other snaps:
327+
328+ * wifi-ap: provides the AP function
329+ * network-manager: handles networking (as a part of installation, the device netplan is modified to make network-manager the renderer for all networking).
330+
331+Wifi-connect can be:
332+
333+ * Installed at run time
334+ * Integrated into an image, with options. See "Integrating into an Image" section
335+
336diff --git a/docs/installation.md b/docs/installation.md
337new file mode 100644
338index 0000000..deba1ec
339--- /dev/null
340+++ b/docs/installation.md
341@@ -0,0 +1,121 @@
342+---
343+title: "Installation"
344+table_of_contents: True
345+---
346+
347+# Overview
348+
349+The wifi-connect snap is currently publish in edge and beta channels.
350+
351+## Install snaps
352+
353+```bash
354+snap install wifi-ap
355+snap install network-manager
356+snap install --edge|beta wifi-connect
357+```
358+
359+## Connect interfaces
360+
361+```bash
362+snap connect wifi-connect:control wifi-ap:control
363+snap connect wifi-connect:network core:network
364+snap connect wifi-connect:network-bind core:network-bind
365+snap connect wifi-connect:network-manager network-manager:service
366+```
367+
368+## Set NetWorkManager to control all networking
369+
370+**Note**: This is a temporary manual step before network-manager snap provides a config option for this.
371+
372+**Note**: Depending on your environment, after this you may need to use a new IP address to connect to the device.
373+
374+ 1. Backup the existing /etc/netplan/00-snapd-config.yaml file
375+
376+ sudo mv /etc/netplan/00-snapd-config.yaml ~/
377+
378+ 1. Create a new netplan config file named /etc/netplan/00-default-nm-renderer.yaml:
379+
380+ sudo vi /etc/netplan/00-default-nm-renderer.yaml
381+
382+ Add the following two lines:
383+
384+ network:
385+ renderer: NetworkManager
386+
387+## Reboot
388+
389+Rebooting addresses a potential content sharing interface issue.
390+
391+Rebooting also consolidates all networking into NetworkManager.
392+
393+## Optionally configure wifi-ap SSID/passphrase
394+
395+If you skip these steps, the wifi-AP put up by the device has an SSID of "Ubuntu" and is unsecure (with no passphrase).
396+
397+ 1. Set the wifi-ap AP SSID
398+
399+ sudo wifi-connect ssid MYSSID
400+
401+ 1. Set the AP passphrase:
402+
403+ sudo wifi-connect passphrase MYPASSPHRASE
404+
405+## Display the AP config
406+
407+```bash
408+sudo wifi-connect show-ap
409+```
410+
411+**Note** the DHCP range:
412+
413+ dhcp.range-start: 10.0.60.2
414+ dhcp.range-stop: 10.0.60.199
415+
416+## Set the portal password
417+
418+The portal password must be entered to access wifi-connect web pages.
419+
420+```bash
421+sudo wifi-connect set-portal-password PASSWORD
422+```
423+
424+## Join the device AP
425+
426+When the device AP is up and available to you, join it.
427+
428+## Open the the Management portal web page
429+
430+This portal displays external wifi APs and let's you join them.
431+
432+After you connect to the device AP, you can open its http portal at the .1 IP address just before the start of the DHCP range (see previous steps) using port 8080:
433+
434+ 10.0.60.1:8080
435+
436+You then need to enter the portal password to continue.
437+
438+### Avahi and hostname
439+
440+You can also connect to the device's web page using the device host name:
441+
442+ http://HOSTNAME.local:8080
443+
444+Where HOSTNAME is the hostname of the device when it booted. (Changing hostname with the hostname command at run time is not sufficient.)
445+
446+**Note**: The system trying to open the web page must support Avahi. Android systems may not, for example.
447+
448+## Be patient, it takes minutes
449+
450+Wifi-connect pauses for about a minute at daemon start to allow any external AP connections to complete.
451+
452+## Disconnect from wifi
453+
454+When connected to an external AP, the Operational portal is available on the device IP address (assigned by the external AP). Open it using IP:8080, enter the portal password, and you may then disconnect with the "Disconnect from Wifi" button.
455+
456+You can also ssh to the device and:
457+
458+ * Use `nmcli c` to display connections.
459+ * Use `nmcli c delete CONNECTION_NAME` to disconnect and delete. This puts the device into management mode, bringing up the AP and portal.
460+
461+Disconnecting sets the device back in Management mode. Its AP is started and you can open the portal (as discussed above) to see external APs and connect to one.
462+
463diff --git a/docs/integrate.md b/docs/integrate.md
464new file mode 100644
465index 0000000..b1cdd4f
466--- /dev/null
467+++ b/docs/integrate.md
468@@ -0,0 +1,70 @@
469+---
470+title: "Integrating into an image""
471+table_of_contents: True
472+---
473+
474+# Overview
475+
476+When you pre-install wifi-connect snap into an image, you can use the gadget snap's [gadget.yaml](https://forum.snapcraft.io/t/the-gadget-snap/696) file to pre-configure some options.
477+
478+Here we explain the snap key and value needed to preconfigure. Refer to the above like for details on how to set these in the gadget snap's gadget.yaml file.
479+
480+You may also set these at run time from terminal with:
481+
482+```bash
483+$ snap set wifi-connect KEY=VALUE
484+```
485+
486+To apply such run-time changes, see the Frequently asked questions page.
487+
488+**Warning**: These changes may create security risks. Only use take these steps if you are completely aware of take responsitbility for the potential risk.
489+
490+
491+**Note** When the deamon starts, it logs any preconfigurations found and applied, for example:
492+
493+```bash
494+Jun 20 22:07:16 thehost snap[18004]: == wifi-connect/SetDefaults portal password being set
495+Jun 20 22:07:16 thehost snap[18004]: == wifi-connect/SetDefaults: reset creds requirement is now disabled
496+```
497+
498+## AP passphrase
499+
500+Normally the wifi-conect AP's passphrase is randomly created by the wifi-ap snap. A normal part of the installation process is resetting this from the terminal. However, some integrators may want to preset the passphrase.
501+
502+Preset the passphrase with the following:
503+
504+ * snapd key: wifi.security-passphrase
505+ * value: the passhrase (8-13 characters, must start with a letter)
506+
507+## Portal password
508+
509+To access any wifi-connect web page you need to enter the portal password. A normal part of the installation process is setting this from the terminal. However, some integrators may want to preset the portal password.
510+
511+Preset the portal password with the following:
512+
513+ * snapd key: portal.password
514+ * value: the password (8-13 characters, must start with a letter)
515+
516+## Disable credential resetting
517+
518+Normally, the first user to access the Management portal is required to reset the wifi-connect AP passphrase and the portal password (used to access wifi-connect web pages). This is an important security feature, especially for integrators that preset these in their image because every image has the same passphrase and password.
519+
520+However, some integrators may want to disable this requirement.
521+
522+Disable the requirement to reset the AP passphrase and portal password on first use of the Management portal as follows:
523+
524+ * snapd key: portal.no-reset-creds
525+ * value: true
526+
527+## Disable display of the Operational Portal
528+
529+The Operational portal is available when the device is connected to an external AP. It provides a button the user can click to disconnect. Display of this page can be disabled. This is a normal part of wifi-connect. When the page is disabled one must use the terminal (or some other means) to direct the device to disconnect form the external AP.
530+
531+Disable the Operational portal as follows:
532+
533+ * snapd key: portal.no-opertaional
534+ * value: true
535+
536+
537+
538+
539diff --git a/docs/metadata.yaml b/docs/metadata.yaml
540new file mode 100644
541index 0000000..2fb6ea0
542--- /dev/null
543+++ b/docs/metadata.yaml
544@@ -0,0 +1,12 @@
545+site_title: wifi-connect
546+site_logo_url: https://assets.ubuntu.com/v1/c5cb0f8e-picto-ubuntu.svg
547+navigation:
548+ - title: Introduction
549+ location: index.md
550+ children:
551+ - title: Installation
552+ location: installation.md
553+ - title: Integrating into an image
554+ location: integrate.md
555+ - title: Frequently asked questions
556+ location: faq.md
557diff --git a/hooks/configure.go b/hooks/configure.go
558new file mode 100644
559index 0000000..601a93a
560--- /dev/null
561+++ b/hooks/configure.go
562@@ -0,0 +1,111 @@
563+// -*- Mode: Go; indent-tabs-mode: t -*-
564+
565+/*
566+ * Copyright (C) 2017 Canonical Ltd
567+ *
568+ * This program is free software: you can redistribute it and/or modify
569+ * it under the terms of the GNU General Public License version 3 as
570+ * published by the Free Software Foundation.
571+ *
572+ * This program is distributed in the hope that it will be useful,
573+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
574+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
575+ * GNU General Public License for more details.
576+ *
577+ * You should have received a copy of the GNU General Public License
578+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
579+ *
580+ */
581+
582+package main
583+
584+import (
585+ "encoding/json"
586+ "io/ioutil"
587+ "log"
588+ "os/exec"
589+ "strings"
590+
591+ "launchpad.net/wifi-connect/daemon"
592+)
593+
594+// Client is the base struct for runtime and testing
595+type Client struct {
596+ getter Getter
597+}
598+
599+// Get is the test obj for overridding functions
600+type Get struct{}
601+
602+// Getter interface is for overriding SnapGet for testing
603+type Getter interface {
604+ SnapGet(string) (string, error)
605+}
606+
607+// GetClient returns a normal runtime client
608+func GetClient() *Client {
609+ return &Client{getter: &Get{}}
610+}
611+
612+// ModClient returns a testing client
613+func ModClient(g Getter) *Client {
614+ return &Client{getter: g}
615+}
616+
617+// SnapGet uses snapctrl to get a value from a key, or returns error
618+func (g *Get) SnapGet(key string) (string, error) {
619+ out, err := exec.Command("snapctl", "get", key).Output()
620+ if err != nil {
621+ return "", err
622+ }
623+ return strings.TrimSpace(string(out)), nil
624+
625+}
626+
627+// snapGetStr wraps SnapGet for string types and verifies the snap var is valid
628+func (c *Client) snapGetStr(key string, target *string) {
629+ val, err := c.getter.SnapGet(key)
630+ if err != nil {
631+ return
632+ }
633+ if len(val) == 0 {
634+ log.Printf("== wifi-connect/configure error: key %s exists but has zero length", key)
635+ return
636+ }
637+ *target = val
638+}
639+
640+// snapGetBool wraps SnapGet for bool types and verifies the snap var is valid
641+func (c *Client) snapGetBool(key string, target *bool) {
642+ val, err := c.getter.SnapGet(key)
643+ if err != nil {
644+ return
645+ }
646+ if len(val) == 0 {
647+ log.Printf("== wifi-connect/configure error: key %s exists but has zero length", key)
648+ return
649+ }
650+
651+ if val == "true" {
652+ *target = true
653+ } else {
654+ *target = false
655+ }
656+}
657+
658+func main() {
659+ client := GetClient()
660+ preConfig := &daemon.PreConfig{}
661+ client.snapGetStr("wifi.security-passphrase", &preConfig.Passphrase)
662+ client.snapGetStr("portal.password", &preConfig.Password)
663+ client.snapGetBool("portal.no-operational", &preConfig.NoOperational)
664+ client.snapGetBool("portal.no-reset-creds", &preConfig.NoResetCreds)
665+
666+ b, errJM := json.Marshal(preConfig)
667+ if errJM == nil {
668+ errWJ := ioutil.WriteFile(daemon.PreConfigFile, b, 0644)
669+ if errWJ != nil {
670+ log.Print("== wifi-connect/configure error:", errWJ)
671+ }
672+ }
673+}
674diff --git a/hooks/configure_test.go b/hooks/configure_test.go
675new file mode 100644
676index 0000000..8aebd68
677--- /dev/null
678+++ b/hooks/configure_test.go
679@@ -0,0 +1,110 @@
680+// -*- Mode: Go; indent-tabs-mode: t -*-
681+
682+/*
683+ * Copyright (C) 2017 Canonical Ltd
684+ *
685+ * This program is free software: you can redistribute it and/or modify
686+ * it under the terms of the GNU General Public License version 3 as
687+ * published by the Free Software Foundation.
688+ *
689+ * This program is distributed in the hope that it will be useful,
690+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
691+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
692+ * GNU General Public License for more details.
693+ *
694+ * You should have received a copy of the GNU General Public License
695+ * along with this program. If not, see <http://www.gnu.org/licenses/>.
696+ *
697+ */
698+
699+package main
700+
701+import (
702+ "fmt"
703+ "testing"
704+)
705+
706+type mock0 struct{}
707+
708+func (mock *mock0) SnapGet(s string) (string, error) {
709+ return "snapgetreturn", nil
710+}
711+
712+type Config struct {
713+ AString string
714+ ABool bool
715+}
716+
717+func TestSnapGetStr0(t *testing.T) {
718+ c := ModClient(&mock0{})
719+ config := &Config{}
720+ config.AString = "defaultstring"
721+ c.snapGetStr("key", &config.AString)
722+ if config.AString != "snapgetreturn" {
723+ t.Errorf("snapGetStr expected snapgetreturn but got %s", config.AString)
724+ }
725+}
726+
727+type mock1 struct{}
728+
729+func (mock *mock1) SnapGet(s string) (string, error) {
730+ return "", fmt.Errorf("intentional error 1")
731+}
732+
733+func TestSnapGetStr1(t *testing.T) {
734+ c := ModClient(&mock1{})
735+ config := &Config{}
736+ config.AString = "defaultstring"
737+ c.snapGetStr("key", &config.AString)
738+ if config.AString != "defaultstring" {
739+ t.Errorf("snapGetStr expected defaultstring but got %s", config.AString)
740+ }
741+}
742+
743+type mock2 struct{}
744+
745+func (mock *mock2) SnapGet(s string) (string, error) {
746+ return "true", nil
747+}
748+
749+func TestSnapGetBool0(t *testing.T) {
750+ c := ModClient(&mock2{})
751+ config := &Config{}
752+ config.ABool = false
753+ c.snapGetBool("key", &config.ABool)
754+ if !config.ABool {
755+ t.Errorf("snapGetBool should be true but is %t", config.ABool)
756+ }
757+}
758+
759+type mock3 struct{}
760+
761+func (mock *mock3) SnapGet(s string) (string, error) {
762+ return "false", nil
763+}
764+
765+func TestSnapGetBool1(t *testing.T) {
766+ c := ModClient(&mock3{})
767+ config := &Config{}
768+ config.ABool = true
769+ c.snapGetBool("key", &config.ABool)
770+ if config.ABool {
771+ t.Errorf("snapGetBool should be false but is %t", config.ABool)
772+ }
773+}
774+
775+type mock4 struct{}
776+
777+func (mock *mock4) SnapGet(s string) (string, error) {
778+ return "", nil
779+}
780+
781+func TestSnapGetBool2(t *testing.T) {
782+ c := ModClient(&mock4{})
783+ config := &Config{}
784+ config.ABool = true
785+ c.snapGetBool("key", &config.ABool)
786+ if !config.ABool {
787+ t.Errorf("snapGetBool should be true but is %t", config.ABool)
788+ }
789+}
790diff --git a/service/service.go b/service/service.go
791index ec39f5f..c988f55 100644
792--- a/service/service.go
793+++ b/service/service.go
794@@ -29,16 +29,22 @@ import (
795 )
796
797 func main() {
798-
799+ c := netman.DefaultClient()
800+ cw := wifiap.DefaultClient()
801 client := daemon.GetClient()
802- client.SetDefaults()
803+
804+ config, err := daemon.LoadPreConfig()
805+ if err != nil {
806+ fmt.Println("== wifi-connect/daemon: preconfiguration error:", err)
807+ }
808+ err = client.SetDefaults(cw, config)
809+ if err != nil {
810+ fmt.Println("== wifi-connect/daemon: SetDetaults error:", err)
811+ }
812 first := true
813 client.SetWaitFlagPath(os.Getenv("SNAP_COMMON") + "/startingApConnect")
814 client.SetManualFlagPath(os.Getenv("SNAP_COMMON") + "/manualMode")
815
816- c := netman.DefaultClient()
817- cw := wifiap.DefaultClient()
818-
819 client.ManagementServerDown()
820 client.OperationalServerDown()
821
822@@ -95,7 +101,9 @@ func main() {
823 if client.GetPreviousState() == daemon.MANAGING {
824 client.ManagementServerDown()
825 }
826- client.OperationalServerUp()
827+ if !config.NoOperational {
828+ client.OperationalServerUp()
829+ }
830 continue
831 }
832
833diff --git a/snap/snapcraft.yaml b/snap/snapcraft.yaml
834index 8bd7f04..c5fb86e 100644
835--- a/snap/snapcraft.yaml
836+++ b/snap/snapcraft.yaml
837@@ -1,8 +1,8 @@
838 name: wifi-connect
839-version: 0.10-1
840+version: 0.10-2-dev
841 summary: Connect your device to external wifi over temp wifi AP
842 description: |
843- A solution to enable your device to connect to ian external
844+ A solution to enable your device to connect to an external
845 wifi AP using a temporary wifi AP the device puts up and then
846 opening its web portal. Note that wifi-connect daemon assumes
847 control of device network management and other management solutions
848@@ -21,6 +21,10 @@ apps:
849 daemon: simple
850 plugs: [network-manager, control, network-bind]
851
852+hooks:
853+ configure:
854+ plugs: [network]
855+
856 plugs:
857 control:
858 interface: content
859@@ -46,9 +50,10 @@ parts:
860 export GOPATH=$PWD/../go
861 cd $GOPATH/src/launchpad.net/wifi-connect
862 ./run-checks all
863+ mkdir -p $SNAPCRAFT_PART_INSTALL/snap/hooks
864+ mv $SNAPCRAFT_PART_INSTALL/bin/hooks $SNAPCRAFT_PART_INSTALL/snap/hooks/configure
865 assets:
866 plugin: dump
867 source: .
868 stage:
869 - static
870-
871diff --git a/static/tests/pre-config0.json b/static/tests/pre-config0.json
872new file mode 100644
873index 0000000..ad904cd
874--- /dev/null
875+++ b/static/tests/pre-config0.json
876@@ -0,0 +1,7 @@
877+{
878+ "wifi.security-passphrase": "abcdefghijklmnop",
879+ "portal.password": "qwerzxcv",
880+ "portal.no-operational": true,
881+ "portal.no-reset-creds": true
882+}
883+
884diff --git a/static/tests/pre-config1.json b/static/tests/pre-config1.json
885new file mode 100644
886index 0000000..f28c4ae
887--- /dev/null
888+++ b/static/tests/pre-config1.json
889@@ -0,0 +1,5 @@
890+{
891+ "portal.no-operational": false,
892+ "portal.no-reset-creds": false
893+}
894+
895diff --git a/wifiap/wifiap.go b/wifiap/wifiap.go
896index b609a55..faf0c15 100644
897--- a/wifiap/wifiap.go
898+++ b/wifiap/wifiap.go
899@@ -25,6 +25,11 @@ import (
900 "time"
901 )
902
903+const (
904+ minPassphraseLen int = 8
905+ maxPassphraseLen int = 63
906+)
907+
908 // Client struct exposing wifi-ap operations
909 type Client struct {
910 restClient *RestClient
911@@ -40,6 +45,16 @@ func DefaultClient() *Client {
912 return &Client{restClient: defaultRestClient()}
913 }
914
915+// Operations interface enables mock testing
916+type Operations interface {
917+ Show() (map[string]interface{}, error)
918+ Enabled() (bool, error)
919+ Enable() error
920+ Disable() error
921+ SetSsid(string) error
922+ SetPassphrase(string) error
923+}
924+
925 func defaultServiceURI() string {
926 return fmt.Sprintf("http://unix%s", filepath.Join(versionURI, configurationURI))
927 }
928@@ -163,8 +178,11 @@ func (client *Client) SetSsid(ssid string) error {
929
930 // SetPassphrase sets the credential to access the wifi ap
931 func (client *Client) SetPassphrase(passphrase string) error {
932- if len(passphrase) < 13 {
933- return fmt.Errorf("Passphrase must be at least 13 chars in length. Please try again")
934+ if len(passphrase) < minPassphraseLen {
935+ return fmt.Errorf("Passphrase must be at least 8 chars in length. Please try again")
936+ }
937+ if len(passphrase) > maxPassphraseLen {
938+ return fmt.Errorf("Passphrase must be less than 64 long. Please try again")
939 }
940
941 params := map[string]string{

Subscribers

People subscribed via source and target branches