2019-03-01 00:23:03 +00:00
|
|
|
# Copyright 2019 Canonical, Ltd.
|
|
|
|
#
|
|
|
|
# This program is free software: you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU Affero General Public License as
|
|
|
|
# published by the Free Software Foundation, either version 3 of the
|
|
|
|
# License, or (at your option) any later version.
|
|
|
|
#
|
|
|
|
# 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
|
|
|
|
# GNU Affero General Public License for more details.
|
|
|
|
#
|
|
|
|
# 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/>.
|
|
|
|
|
2019-12-02 23:28:05 +00:00
|
|
|
import asyncio
|
|
|
|
from functools import partial
|
2019-03-10 20:09:35 +00:00
|
|
|
import glob
|
2019-03-01 00:23:03 +00:00
|
|
|
import json
|
|
|
|
import logging
|
|
|
|
import os
|
2019-03-07 02:05:13 +00:00
|
|
|
import time
|
2019-03-01 00:23:03 +00:00
|
|
|
from urllib.parse import (
|
|
|
|
quote_plus,
|
|
|
|
urlencode,
|
|
|
|
)
|
|
|
|
|
2019-12-12 03:03:58 +00:00
|
|
|
from subiquitycore.async_helpers import run_in_thread
|
2019-03-07 02:05:13 +00:00
|
|
|
from subiquitycore.utils import run_command
|
|
|
|
|
2019-03-01 00:23:03 +00:00
|
|
|
import requests_unixsocket
|
|
|
|
|
|
|
|
|
2020-07-02 00:56:49 +00:00
|
|
|
log = logging.getLogger('subiquitycore.snapd')
|
2019-03-01 00:23:03 +00:00
|
|
|
|
2019-03-07 02:46:16 +00:00
|
|
|
# Every method in this module blocks. Do not call them from the main thread!
|
|
|
|
|
2019-03-01 00:23:03 +00:00
|
|
|
|
|
|
|
class SnapdConnection:
|
2019-03-07 02:05:13 +00:00
|
|
|
def __init__(self, root, sock):
|
|
|
|
self.root = root
|
2019-03-01 00:23:03 +00:00
|
|
|
self.url_base = "http+unix://{}/".format(quote_plus(sock))
|
|
|
|
|
|
|
|
def get(self, path, **args):
|
|
|
|
if args:
|
|
|
|
path += '?' + urlencode(args)
|
2021-10-12 09:11:20 +00:00
|
|
|
with requests_unixsocket.Session() as session:
|
|
|
|
return session.get(self.url_base + path, timeout=60)
|
2019-03-01 00:23:03 +00:00
|
|
|
|
2019-03-07 21:43:28 +00:00
|
|
|
def post(self, path, body, **args):
|
|
|
|
if args:
|
|
|
|
path += '?' + urlencode(args)
|
2021-10-12 09:11:20 +00:00
|
|
|
with requests_unixsocket.Session() as session:
|
|
|
|
return session.post(
|
|
|
|
self.url_base + path, data=json.dumps(body),
|
|
|
|
timeout=60)
|
2019-03-07 21:43:28 +00:00
|
|
|
|
2019-03-07 02:05:13 +00:00
|
|
|
def configure_proxy(self, proxy):
|
|
|
|
log.debug("restarting snapd to pick up proxy config")
|
|
|
|
dropin_dir = os.path.join(
|
|
|
|
self.root, 'etc/systemd/system/snapd.service.d')
|
|
|
|
os.makedirs(dropin_dir, exist_ok=True)
|
|
|
|
with open(os.path.join(dropin_dir, 'snap_proxy.conf'), 'w') as fp:
|
|
|
|
fp.write(proxy.proxy_systemd_dropin())
|
|
|
|
if self.root == '/':
|
|
|
|
cmds = [
|
|
|
|
['systemctl', 'daemon-reload'],
|
|
|
|
['systemctl', 'restart', 'snapd.service'],
|
|
|
|
]
|
|
|
|
else:
|
|
|
|
cmds = [['sleep', '2']]
|
|
|
|
for cmd in cmds:
|
|
|
|
run_command(cmd)
|
|
|
|
|
2019-03-01 00:23:03 +00:00
|
|
|
|
2019-03-07 21:43:28 +00:00
|
|
|
class _FakeFileResponse:
|
2019-03-01 00:23:03 +00:00
|
|
|
|
|
|
|
def __init__(self, path):
|
|
|
|
self.path = path
|
|
|
|
|
|
|
|
def raise_for_status(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
with open(self.path) as fp:
|
|
|
|
return json.load(fp)
|
|
|
|
|
|
|
|
|
2019-03-07 21:43:28 +00:00
|
|
|
class _FakeMemoryResponse:
|
|
|
|
|
|
|
|
def __init__(self, data):
|
|
|
|
self.data = data
|
|
|
|
|
|
|
|
def raise_for_status(self):
|
|
|
|
pass
|
|
|
|
|
|
|
|
def json(self):
|
|
|
|
return self.data
|
|
|
|
|
|
|
|
|
2019-03-10 20:09:35 +00:00
|
|
|
class ResponseSet:
|
|
|
|
"""Responses for a endpoint that returns different data each time.
|
|
|
|
|
|
|
|
Motivating example is v2/changes/$change_id."""
|
|
|
|
|
|
|
|
def __init__(self, files):
|
|
|
|
self.files = files
|
|
|
|
self.index = 0
|
|
|
|
|
|
|
|
def next(self):
|
|
|
|
f = self.files[self.index]
|
|
|
|
d = int(os.environ.get("SUBIQUITY_REPLAY_TIMESCALE", 1))
|
|
|
|
# Make sure we return the last response even when we skip most
|
|
|
|
# of them.
|
|
|
|
if d > 1 and self.index + d >= len(self.files):
|
|
|
|
self.index = len(self.files) - 1
|
|
|
|
else:
|
|
|
|
self.index += d
|
|
|
|
return _FakeFileResponse(f)
|
|
|
|
|
|
|
|
|
2019-03-01 00:23:03 +00:00
|
|
|
class FakeSnapdConnection:
|
2022-01-25 21:10:40 +00:00
|
|
|
def __init__(self, snap_data_dir, scale_factor, output_base):
|
2019-03-01 00:23:03 +00:00
|
|
|
self.snap_data_dir = snap_data_dir
|
2020-04-01 03:49:51 +00:00
|
|
|
self.scale_factor = scale_factor
|
2019-03-10 20:09:35 +00:00
|
|
|
self.response_sets = {}
|
2022-01-25 21:10:40 +00:00
|
|
|
self.output_base = output_base
|
2019-03-01 00:23:03 +00:00
|
|
|
|
2019-03-07 02:05:13 +00:00
|
|
|
def configure_proxy(self, proxy):
|
|
|
|
log.debug("pretending to restart snapd to pick up proxy config")
|
2020-04-01 03:49:51 +00:00
|
|
|
time.sleep(2/self.scale_factor)
|
2019-03-07 02:05:13 +00:00
|
|
|
|
2019-03-07 21:43:28 +00:00
|
|
|
def post(self, path, body, **args):
|
|
|
|
if path == "v2/snaps/subiquity" and body['action'] == 'refresh':
|
2021-03-30 23:19:55 +00:00
|
|
|
# The post-refresh hook does this in the real world.
|
2022-01-25 21:10:40 +00:00
|
|
|
update_marker_file = self.output_base + '/run/subiquity/updating'
|
2021-03-30 23:19:55 +00:00
|
|
|
open(update_marker_file, 'w').close()
|
2019-03-07 21:43:28 +00:00
|
|
|
return _FakeMemoryResponse({
|
|
|
|
"type": "async",
|
2021-02-09 22:43:58 +00:00
|
|
|
"change": "7",
|
2019-03-07 21:43:28 +00:00
|
|
|
"status-code": 200,
|
|
|
|
"status": "OK",
|
|
|
|
})
|
2019-03-29 01:45:11 +00:00
|
|
|
if path == "v2/snaps/subiquity" and body['action'] == 'switch':
|
|
|
|
return _FakeMemoryResponse({
|
|
|
|
"type": "async",
|
2021-02-09 22:43:58 +00:00
|
|
|
"change": "8",
|
2019-03-29 01:45:11 +00:00
|
|
|
"status-code": 200,
|
|
|
|
"status": "Accepted",
|
|
|
|
})
|
2019-03-07 21:43:28 +00:00
|
|
|
raise Exception(
|
|
|
|
"Don't know how to fake POST response to {}".format((path, args)))
|
|
|
|
|
2019-03-01 00:23:03 +00:00
|
|
|
def get(self, path, **args):
|
2022-10-05 01:37:34 +00:00
|
|
|
time.sleep(1/self.scale_factor)
|
2019-03-01 00:23:03 +00:00
|
|
|
filename = path.replace('/', '-')
|
|
|
|
if args:
|
|
|
|
filename += '-' + urlencode(sorted(args.items()))
|
2019-03-10 20:09:35 +00:00
|
|
|
if filename in self.response_sets:
|
|
|
|
return self.response_sets[filename].next()
|
2019-03-07 21:43:28 +00:00
|
|
|
filepath = os.path.join(self.snap_data_dir, filename)
|
|
|
|
if os.path.exists(filepath + '.json'):
|
|
|
|
return _FakeFileResponse(filepath + '.json')
|
|
|
|
if os.path.isdir(filepath):
|
2019-03-10 20:09:35 +00:00
|
|
|
files = sorted(glob.glob(os.path.join(filepath, '*.json')))
|
|
|
|
rs = self.response_sets[filename] = ResponseSet(files)
|
|
|
|
return rs.next()
|
2019-03-01 00:23:03 +00:00
|
|
|
raise Exception(
|
2019-03-07 21:43:28 +00:00
|
|
|
"Don't know how to fake GET response to {}".format((path, args)))
|
2019-12-02 23:28:05 +00:00
|
|
|
|
|
|
|
|
|
|
|
class AsyncSnapd:
|
|
|
|
|
|
|
|
def __init__(self, connection):
|
|
|
|
self.connection = connection
|
|
|
|
|
|
|
|
async def get(self, path, **args):
|
|
|
|
response = await run_in_thread(
|
|
|
|
partial(self.connection.get, path, **args))
|
|
|
|
response.raise_for_status()
|
|
|
|
return response.json()
|
|
|
|
|
|
|
|
async def post(self, path, body, **args):
|
|
|
|
response = await run_in_thread(
|
|
|
|
partial(self.connection.post, path, body, **args))
|
|
|
|
response.raise_for_status()
|
2022-10-05 01:37:34 +00:00
|
|
|
return response.json()['change']
|
2019-12-02 23:28:05 +00:00
|
|
|
|
|
|
|
async def post_and_wait(self, path, body, **args):
|
2022-10-05 01:37:34 +00:00
|
|
|
change = await self.post(path, body, **args)
|
2019-12-02 23:28:05 +00:00
|
|
|
change_path = 'v2/changes/{}'.format(change)
|
|
|
|
while True:
|
|
|
|
result = await self.get(change_path)
|
|
|
|
if result["result"]["status"] == "Done":
|
|
|
|
break
|
|
|
|
await asyncio.sleep(0.1)
|