Merge pull request #1872 from kubiko/console-conf-use-snapd-socket
console_conf: identity: use snapd unix socket
This commit is contained in:
commit
d617ae0f1f
|
@ -13,7 +13,6 @@
|
||||||
# You should have received a copy of the GNU Affero General Public License
|
# You should have received a copy of the GNU Affero General Public License
|
||||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
import json
|
|
||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import pwd
|
import pwd
|
||||||
|
@ -25,7 +24,7 @@ from subiquitycore import snap
|
||||||
from subiquitycore.snapd import SnapdConnection
|
from subiquitycore.snapd import SnapdConnection
|
||||||
from subiquitycore.ssh import get_ips_standalone, host_key_info
|
from subiquitycore.ssh import get_ips_standalone, host_key_info
|
||||||
from subiquitycore.tuicontroller import TuiController
|
from subiquitycore.tuicontroller import TuiController
|
||||||
from subiquitycore.utils import disable_console_conf, run_command
|
from subiquitycore.utils import disable_console_conf
|
||||||
|
|
||||||
log = logging.getLogger("console_conf.controllers.identity")
|
log = logging.getLogger("console_conf.controllers.identity")
|
||||||
|
|
||||||
|
@ -52,9 +51,8 @@ def get_core_version():
|
||||||
return version
|
return version
|
||||||
|
|
||||||
|
|
||||||
def get_managed():
|
def get_managed(con):
|
||||||
"""Check if device is managed"""
|
"""Check if device is managed"""
|
||||||
con = SnapdConnection("", "/run/snapd.socket")
|
|
||||||
return con.get("v2/system-info").json()["result"]["managed"]
|
return con.get("v2/system-info").json()["result"]["managed"]
|
||||||
|
|
||||||
|
|
||||||
|
@ -66,9 +64,8 @@ def get_realname(username):
|
||||||
return info.pw_gecos.split(",", 1)[0]
|
return info.pw_gecos.split(",", 1)[0]
|
||||||
|
|
||||||
|
|
||||||
def get_device_owner():
|
def get_device_owner(con):
|
||||||
"""Get device owner, if any"""
|
"""Get device owner, if any"""
|
||||||
con = SnapdConnection("", "/run/snapd.socket")
|
|
||||||
for user in con.get("v2/users").json()["result"]:
|
for user in con.get("v2/users").json()["result"]:
|
||||||
if "username" not in user:
|
if "username" not in user:
|
||||||
continue
|
continue
|
||||||
|
@ -130,7 +127,9 @@ def write_login_details(fp, username, ips, state_dir=None):
|
||||||
|
|
||||||
|
|
||||||
def write_login_details_standalone():
|
def write_login_details_standalone():
|
||||||
owner = get_device_owner()
|
# running in standalone mode
|
||||||
|
con = SnapdConnection("/", "/run/snapd.socket")
|
||||||
|
owner = get_device_owner(con)
|
||||||
ips = get_ips_standalone()
|
ips = get_ips_standalone()
|
||||||
if len(ips) == 0:
|
if len(ips) == 0:
|
||||||
if owner is None:
|
if owner is None:
|
||||||
|
@ -160,8 +159,8 @@ class IdentityController(TuiController):
|
||||||
self.model = app.base_model.identity
|
self.model = app.base_model.identity
|
||||||
|
|
||||||
def make_ui(self):
|
def make_ui(self):
|
||||||
if get_managed():
|
if get_managed(self.app.snapdcon):
|
||||||
device_owner = get_device_owner()
|
device_owner = get_device_owner(self.app.snapdcon)
|
||||||
if device_owner:
|
if device_owner:
|
||||||
self.model.add_user(device_owner)
|
self.model.add_user(device_owner)
|
||||||
return self.make_login_view()
|
return self.make_login_view()
|
||||||
|
@ -178,18 +177,19 @@ class IdentityController(TuiController):
|
||||||
login_details_path = self.opts.output_base + "/login-details.txt"
|
login_details_path = self.opts.output_base + "/login-details.txt"
|
||||||
else:
|
else:
|
||||||
self.app.urwid_loop.draw_screen()
|
self.app.urwid_loop.draw_screen()
|
||||||
cp = run_command(["snap", "create-user", "--sudoer", "--json", email])
|
user_action = {"action": "create", "email": email, "sudoer": True}
|
||||||
if cp.returncode != 0:
|
res = self.app.snapdcon.post("v2/users", body=user_action)
|
||||||
|
if res.json()["status"] != "OK":
|
||||||
if isinstance(self.ui.body, IdentityView):
|
if isinstance(self.ui.body, IdentityView):
|
||||||
self.ui.body.snap_create_user_failed(
|
self.ui.body.snap_create_user_failed(
|
||||||
"Creating user failed:", cp.stderr
|
"Creating user failed:", res.json()["result"]["message"]
|
||||||
)
|
)
|
||||||
return
|
return
|
||||||
else:
|
else:
|
||||||
data = json.loads(cp.stdout)
|
username = res.json()["result"][0]["username"]
|
||||||
result = {
|
result = {
|
||||||
"realname": email,
|
"realname": email,
|
||||||
"username": data["username"],
|
"username": username,
|
||||||
}
|
}
|
||||||
login_details_path = self.app.state_path("login-details.txt")
|
login_details_path = self.app.state_path("login-details.txt")
|
||||||
self.model.add_user(result)
|
self.model.add_user(result)
|
||||||
|
|
|
@ -18,24 +18,22 @@ import unittest
|
||||||
from unittest.mock import MagicMock, patch
|
from unittest.mock import MagicMock, patch
|
||||||
|
|
||||||
from console_conf.controllers.identity import IdentityController
|
from console_conf.controllers.identity import IdentityController
|
||||||
|
from console_conf.ui.views import IdentityView, LoginView
|
||||||
from subiquitycore.models.network import NetworkDev
|
from subiquitycore.models.network import NetworkDev
|
||||||
|
from subiquitycore.snapd import MemoryResponseSet, get_fake_connection
|
||||||
from subiquitycore.tests.mocks import make_app
|
from subiquitycore.tests.mocks import make_app
|
||||||
|
|
||||||
|
|
||||||
class TestIdentityController(unittest.TestCase):
|
class TestIdentityController(unittest.TestCase):
|
||||||
@patch("os.ttyname", return_value="/dev/tty1")
|
@patch("os.ttyname", return_value="/dev/tty1")
|
||||||
@patch("console_conf.controllers.identity.get_core_version", return_value="24")
|
@patch("console_conf.controllers.identity.get_core_version", return_value="24")
|
||||||
@patch("console_conf.controllers.identity.run_command")
|
def test_snap_integration(self, core_version, ttyname):
|
||||||
def test_snap_integration(self, run_command, core_version, ttyname):
|
|
||||||
with tempfile.TemporaryDirectory(suffix="console-conf-test") as statedir:
|
with tempfile.TemporaryDirectory(suffix="console-conf-test") as statedir:
|
||||||
proc_mock = MagicMock()
|
|
||||||
run_command.return_value = proc_mock
|
|
||||||
proc_mock.returncode = 0
|
|
||||||
proc_mock.stdout = '{"username":"foo"}'
|
|
||||||
|
|
||||||
app = make_app()
|
app = make_app()
|
||||||
app.state_dir = statedir
|
app.state_dir = statedir
|
||||||
app.opts.dry_run = False
|
app.opts.dry_run = False
|
||||||
|
app.snapdcon = get_fake_connection()
|
||||||
|
app.state_dir = statedir
|
||||||
network_model = MagicMock()
|
network_model = MagicMock()
|
||||||
mock_devs = [MagicMock(spec=NetworkDev)]
|
mock_devs = [MagicMock(spec=NetworkDev)]
|
||||||
network_model.get_all_netdevs.return_value = mock_devs
|
network_model.get_all_netdevs.return_value = mock_devs
|
||||||
|
@ -48,12 +46,77 @@ class TestIdentityController(unittest.TestCase):
|
||||||
|
|
||||||
app.state_path = MagicMock(side_effect=state_path)
|
app.state_path = MagicMock(side_effect=state_path)
|
||||||
|
|
||||||
|
create_user_calls = 0
|
||||||
|
|
||||||
|
def create_user_cb(path, body, **args):
|
||||||
|
nonlocal create_user_calls
|
||||||
|
create_user_calls += 1
|
||||||
|
self.assertEqual(path, "v2/users")
|
||||||
|
self.assertEqual(
|
||||||
|
body, {"action": "create", "email": "foo@bar.com", "sudoer": True}
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"status": "OK",
|
||||||
|
"result": [
|
||||||
|
{
|
||||||
|
"username": "foo",
|
||||||
|
}
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
# fake POST handlers
|
||||||
|
app.snapdcon.post_cb["v2/users"] = create_user_cb
|
||||||
|
|
||||||
c = IdentityController(app)
|
c = IdentityController(app)
|
||||||
c.identity_done("foo@bar.com")
|
c.identity_done("foo@bar.com")
|
||||||
run_command.assert_called_with(
|
|
||||||
["snap", "create-user", "--sudoer", "--json", "foo@bar.com"]
|
self.assertEqual(create_user_calls, 1)
|
||||||
)
|
|
||||||
|
|
||||||
with open(os.path.join(statedir, "login-details.txt")) as inf:
|
with open(os.path.join(statedir, "login-details.txt")) as inf:
|
||||||
data = inf.read()
|
data = inf.read()
|
||||||
self.assertIn("Ubuntu Core 24 on 1.2.3.4 (tty1)\n", data)
|
self.assertIn("Ubuntu Core 24 on 1.2.3.4 (tty1)\n", data)
|
||||||
|
|
||||||
|
@patch("pwd.getpwnam")
|
||||||
|
@patch("os.path.isdir", return_value=True)
|
||||||
|
def test_make_ui_managed_with_user(self, isdir, getpwnam):
|
||||||
|
pwinfo = MagicMock()
|
||||||
|
pwinfo.pw_gecos = "Foo,Bar"
|
||||||
|
getpwnam.return_value = pwinfo
|
||||||
|
|
||||||
|
app = make_app()
|
||||||
|
app.opts.dry_run = False
|
||||||
|
app.snapdcon = get_fake_connection()
|
||||||
|
# app.state_dir = statedir
|
||||||
|
network_model = MagicMock()
|
||||||
|
mock_devs = [MagicMock(spec=NetworkDev)]
|
||||||
|
network_model.get_all_netdevs.return_value = mock_devs
|
||||||
|
mock_devs[0].actual_global_ip_addresses = ["1.2.3.4"]
|
||||||
|
app.base_model.network = network_model
|
||||||
|
|
||||||
|
app.snapdcon.response_sets = {
|
||||||
|
"v2-system-info": MemoryResponseSet([{"result": {"managed": True}}]),
|
||||||
|
"v2-users": MemoryResponseSet(
|
||||||
|
[
|
||||||
|
# no "username" for first entry
|
||||||
|
{"result": [{}, {"username": "foo"}]}
|
||||||
|
]
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
c = IdentityController(app)
|
||||||
|
ui = c.make_ui()
|
||||||
|
self.assertIsInstance(ui, LoginView)
|
||||||
|
getpwnam.assert_called_with("foo")
|
||||||
|
|
||||||
|
def test_make_ui_unmanaged(self):
|
||||||
|
app = make_app()
|
||||||
|
app.opts.dry_run = False
|
||||||
|
app.snapdcon = get_fake_connection()
|
||||||
|
|
||||||
|
app.snapdcon.response_sets = {
|
||||||
|
"v2-system-info": MemoryResponseSet([{"result": {"managed": False}}]),
|
||||||
|
}
|
||||||
|
|
||||||
|
c = IdentityController(app)
|
||||||
|
ui = c.make_ui()
|
||||||
|
self.assertIsInstance(ui, IdentityView)
|
||||||
|
|
|
@ -19,6 +19,7 @@ from console_conf.models.console_conf import ConsoleConfModel
|
||||||
from console_conf.models.systems import RecoverySystemsModel
|
from console_conf.models.systems import RecoverySystemsModel
|
||||||
from subiquitycore.prober import Prober
|
from subiquitycore.prober import Prober
|
||||||
from subiquitycore.snap import snap_name
|
from subiquitycore.snap import snap_name
|
||||||
|
from subiquitycore.snapd import SnapdConnection
|
||||||
from subiquitycore.tui import TuiApplication
|
from subiquitycore.tui import TuiApplication
|
||||||
|
|
||||||
log = logging.getLogger("console_conf.core")
|
log = logging.getLogger("console_conf.core")
|
||||||
|
@ -46,6 +47,9 @@ class ConsoleConf(TuiApplication):
|
||||||
def __init__(self, opts):
|
def __init__(self, opts):
|
||||||
super().__init__(opts)
|
super().__init__(opts)
|
||||||
self.prober = Prober(opts.machine_config, self.debug_flags)
|
self.prober = Prober(opts.machine_config, self.debug_flags)
|
||||||
|
# we're talking to snapd over the main socket, this may require
|
||||||
|
# snapd-control if executing inside a snap
|
||||||
|
self.snapdcon = SnapdConnection(self.root, "/run/snapd.socket")
|
||||||
|
|
||||||
|
|
||||||
class RecoveryChooser(TuiApplication):
|
class RecoveryChooser(TuiApplication):
|
||||||
|
|
|
@ -121,12 +121,26 @@ class ResponseSet:
|
||||||
return _FakeFileResponse(f)
|
return _FakeFileResponse(f)
|
||||||
|
|
||||||
|
|
||||||
|
class MemoryResponseSet:
|
||||||
|
"""Set of response for an endpoint which returns data stored in memory."""
|
||||||
|
|
||||||
|
def __init__(self, data):
|
||||||
|
self.data = data
|
||||||
|
self.index = 0
|
||||||
|
|
||||||
|
def next(self):
|
||||||
|
d = self.data[self.index]
|
||||||
|
self.index += 1
|
||||||
|
return _FakeMemoryResponse(d)
|
||||||
|
|
||||||
|
|
||||||
class FakeSnapdConnection:
|
class FakeSnapdConnection:
|
||||||
def __init__(self, snap_data_dir, scale_factor, output_base):
|
def __init__(self, snap_data_dir, scale_factor, output_base):
|
||||||
self.snap_data_dir = snap_data_dir
|
self.snap_data_dir = snap_data_dir
|
||||||
self.scale_factor = scale_factor
|
self.scale_factor = scale_factor
|
||||||
self.response_sets = {}
|
self.response_sets = {}
|
||||||
self.output_base = output_base
|
self.output_base = output_base
|
||||||
|
self.post_cb = {}
|
||||||
|
|
||||||
def configure_proxy(self, proxy):
|
def configure_proxy(self, proxy):
|
||||||
log.debug("pretending to restart snapd to pick up proxy config")
|
log.debug("pretending to restart snapd to pick up proxy config")
|
||||||
|
@ -167,6 +181,9 @@ class FakeSnapdConnection:
|
||||||
"status": "Accepted",
|
"status": "Accepted",
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
if path in self.post_cb:
|
||||||
|
return _FakeMemoryResponse(self.post_cb[path](path, body, **args))
|
||||||
|
|
||||||
raise Exception(
|
raise Exception(
|
||||||
"Don't know how to fake POST response to {}".format((path, args))
|
"Don't know how to fake POST response to {}".format((path, args))
|
||||||
)
|
)
|
||||||
|
|
Loading…
Reference in New Issue