Merge lp:~noise/tanuki-agent/provkit-compose into lp:tanuki-agent

Proposed by Bret Barker
Status: Merged
Approved by: Bret Barker
Approved revision: 168
Merged at revision: 167
Proposed branch: lp:~noise/tanuki-agent/provkit-compose
Merge into: lp:tanuki-agent
Diff against target: 257 lines (+206/-1)
4 files modified
Makefile (+1/-1)
rpi2-sample-provkit/provision2 (+114/-0)
rpi2-sample-provkit/setup (+1/-0)
rpi2-sample-provkit/utils.py (+90/-0)
To merge this branch: bzr merge lp:~noise/tanuki-agent/provkit-compose
Reviewer Review Type Date Requested Status
Roberto Alsina (community) Approve
Review via email: mp+278370@code.launchpad.net

Commit message

1st pass - new provision2 script that knows how to u-d-f images from base_snaps+channel

Description of the change

This will likely require more work after other branches land, but so far will successfully read test opps that have additional base image data, e.g.:
    "base_snaps": [
        { "name": "ubuntu-core", "sequence": 7},
        { "name": "pi2.canonical", "sequence": 3},
        { "name": "test-snap.noise", "sequence": 1}
    ],
    "base_channel": "edge",
    "base_image_id": "qwerty12345",

check CPI to confirm sequence #s are current in base_channel, download the snaps, and compose an image with ubuntu-device-flash.

To post a comment you must log in.
Revision history for this message
Roberto Alsina (ralsina) wrote :

+1

review: Approve

Preview Diff

[H/L] Next/Prev Comment, [J/K] Next/Prev File, [N/P] Next/Prev Hunk
1=== modified file 'Makefile'
2--- Makefile 2015-10-30 21:14:35 +0000
3+++ Makefile 2015-11-23 20:28:24 +0000
4@@ -19,7 +19,7 @@
5
6 raw-lint:
7 env/bin/flake8 *.py scripts/*.py --max-line-length=99
8- env/bin/flake8 rpi2-sample-provkit/setup rpi2-sample-provkit/provision rpi2-sample-provkit/runtest rpi2-sample-provkit/sshrobot --max-line-length=99 --builtins=raw_input
9+ env/bin/flake8 rpi2-sample-provkit/setup rpi2-sample-provkit/provision rpi2-sample-provkit/provision2 rpi2-sample-provkit/runtest rpi2-sample-provkit/sshrobot rpi2-sample-provkit/utils.py --max-line-length=99 --builtins=raw_input
10 env/bin/flake8 rpi2-sample-provkit/test_tarball_content/test --max-line-length=99
11
12 raw-test:
13
14=== added file 'rpi2-sample-provkit/provision2'
15--- rpi2-sample-provkit/provision2 1970-01-01 00:00:00 +0000
16+++ rpi2-sample-provkit/provision2 2015-11-23 20:28:24 +0000
17@@ -0,0 +1,114 @@
18+#!/usr/bin/python -u
19+
20+from __future__ import print_function
21+
22+import argparse
23+import codecs
24+import json
25+import os
26+import subprocess
27+import time
28+
29+import utils
30+
31+MAX_RETRIES = 5
32+
33+STEPS = """
34+1. Write the image into the Raspberry Pi 2 SDCard (remote flashing of RPi2 is
35+ still in-development), in another shell/window:
36+
37+ sudo dd if={} of=SDCARD_DEVICE bs=32M
38+
39+ where SDCARD_DEVICE
40+ is the device in your system that holds the SDCard (like '/dev/sdb',
41+ but be SURE that you're choosing the right device, otherwise you may
42+ overwrite preexisting data on a device in your machine).
43+
44+2. Insert the card into the Raspberry Pi 2, and reboot it.
45+"""
46+
47+log = utils.get_prov_kit_logger()
48+
49+
50+def run_sshrobot(sshopts, cmd):
51+ """Run a command using sshrobot helper."""
52+ sshrobot = os.path.join(os.path.dirname(__file__), 'sshrobot')
53+ invoke = [sshrobot] + sshopts.ssh + [cmd]
54+ p = subprocess.Popen(invoke, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
55+ buf = ""
56+ while True:
57+ r = p.poll()
58+ if r is not None:
59+ out = (buf + p.stdout.read().decode("utf8")).strip()
60+ if out:
61+ print(out)
62+ return r
63+ ch = p.stdout.read(1).decode("utf8")
64+ buf += ch
65+ if buf.endswith(" password: "):
66+ break
67+ p.stdin.write(sshopts.password + "\n")
68+ p.stdin.flush()
69+ r = p.wait()
70+ out = p.stdout.read().decode("utf8").strip()
71+ if out:
72+ print(out)
73+ return r
74+
75+
76+def wait_device(sshopts):
77+ """Wait for device to answer SSH."""
78+ ssh_identity = os.getenv('SSH_IDENTITY', '~/.ssh/id_rsa.pub')
79+ pubpath = os.path.expanduser(ssh_identity)
80+ with open(pubpath, "rb") as fh:
81+ pubcontent = fh.read()
82+ cmd = "echo '" + pubcontent + b"' >> ~/.ssh/authorized_keys"
83+ for attempt_number in range(1, MAX_RETRIES + 1):
84+ res = run_sshrobot(sshopts, cmd)
85+ if res == 0:
86+ return
87+ if attempt_number == MAX_RETRIES:
88+ break
89+ log.info("device still not answering, waiting 10s and retrying {}/{}..."
90+ .format(attempt_number, MAX_RETRIES-1))
91+ time.sleep(10)
92+
93+ # we never reached the raspi in all attempts
94+ log.error("device never answered, quitting!")
95+ log.error("Please be sure that the Raspberry Pi 2 is in a functional state and retry.")
96+ exit(1)
97+
98+
99+def main(args):
100+ """Main entry point."""
101+ sshopts = utils.device_sshopts()
102+ # get the test opportunity content
103+ with codecs.open(args.test_opp, 'rt', encoding='utf8') as fh:
104+ test_opp = json.load(fh)
105+
106+ log.info("Starting to provision the Raspberry Pi 2 (@ {address}:{port}),"
107+ " for the test {spec_name}".format(address=sshopts.ip, port=sshopts.port, **test_opp))
108+
109+ # TODO: test_opp needs base_channel, base_snaps, architecture, base_image_id
110+ arch = test_opp['platform'] # XXX: s/platform/architecture
111+ release = test_opp['release']
112+ channel = test_opp['base_channel']
113+ base_snaps = test_opp['base_snaps']
114+
115+ image_filename = 'gm_{}_{}.img'.format(channel, test_opp['base_image_id'])
116+ utils.compose_image(arch, release, channel, base_snaps, image_filename)
117+
118+ print(STEPS.format(image_filename, **test_opp))
119+ print("Please press ENTER when all steps are done")
120+ raw_input()
121+
122+ # wait for ssh to actually work
123+ wait_device(sshopts)
124+ log.info("Done.")
125+
126+
127+if __name__ == "__main__":
128+ parser = argparse.ArgumentParser()
129+ parser.add_argument("test_opp")
130+ args = parser.parse_args()
131+ main(args)
132
133=== modified file 'rpi2-sample-provkit/setup'
134--- rpi2-sample-provkit/setup 2015-10-30 21:14:35 +0000
135+++ rpi2-sample-provkit/setup 2015-11-23 20:28:24 +0000
136@@ -60,6 +60,7 @@
137
138 # try to update in the running image
139 for snap_name, snap_revno in to_update:
140+ # XXX: use new CPI and download function in utils.py
141 log.info('requesting package info for snap: %s, headers: %r', snap_name, headers)
142 res = requests.get(PACKAGE_URL.format(snap_name), headers=headers)
143 if res.status_code != 200:
144
145=== modified file 'rpi2-sample-provkit/utils.py'
146--- rpi2-sample-provkit/utils.py 2015-10-30 21:14:35 +0000
147+++ rpi2-sample-provkit/utils.py 2015-11-23 20:28:24 +0000
148@@ -1,15 +1,24 @@
149 import logging
150 import os
151 import sys
152+import requests
153+import subprocess
154+
155+
156+PACKAGE_URL = "https://search.apps.ubuntu.com/api/v1/package/{}"
157
158
159 def get_prov_kit_logger():
160 """Configure logging and return the provkit logger."""
161 logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s: %(message)s",
162 stream=sys.stderr, level=logging.INFO)
163+ logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.ERROR)
164 return logging.getLogger("provkit.{}".format(os.path.basename(sys.argv[0])))
165
166
167+log = get_prov_kit_logger()
168+
169+
170 def device_sshopts():
171 opts = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']
172
173@@ -21,3 +30,84 @@
174 ssh = opts + ['-q', '-p', str(port), target]
175 scp = opts + ['-q', '-P', str(port)]
176 return sshopts
177+
178+
179+def get_snap_info(arch, release, channel, snap_name, snap_seq, bail_on_seq_mismatch=True):
180+ """ Fetch package info from Click Package Index (CPI) and return the download URL
181+ and a boolean is_oem if this is an OEM type package. """
182+
183+ headers = {
184+ 'X-Ubuntu-Architecture': arch,
185+ 'X-Ubuntu-Release': release
186+ }
187+
188+ url = PACKAGE_URL.format(snap_name)
189+ log.info('Requesting package info for snap: %s, url: %s, headers: %r', snap_name, url, headers)
190+
191+ res = requests.get(url, headers=headers)
192+ if res.status_code != 200:
193+ log.error('Cannot find package info for snap: %s, status_code: %d',
194+ snap_name, res.status_code)
195+ sys.exit(1)
196+ info = res.json()
197+ if info['sequence'] != snap_seq:
198+ log.error("cannot get hold of {} sequence {}, it's not the current latest: {}"
199+ .format(snap_name, snap_seq, info['sequence']))
200+ sys.exit(1)
201+
202+ url = info['anon_download_url']
203+ is_oem = info['content'] == 'oem'
204+ return url, is_oem
205+
206+
207+def download_snap(url, output_filename):
208+ # TODO: consider keeping a cache of downloads
209+ log.info("Downloading: {}".format(url))
210+ subprocess.check_call(['curl', '-o', output_filename, url])
211+
212+
213+def compose_image(arch, release, channel, base_snaps, image_filename):
214+ """ Gather snap info, downlaod snaps, and use ubuntu-device-flash to compose an image"""
215+ # gather snap info
216+ oem = None
217+ installs = []
218+ for s in base_snaps:
219+ if s['name'] == 'ubuntu-core': # XXX: remove once we have all-snaps
220+ log.warn('Skipping ubuntu-core snap because we are still in hybrid system-image world')
221+ core = s
222+ continue
223+ s['url'], s['is_oem'] = get_snap_info(arch, release, channel, s['name'], s['sequence'])
224+ s['fname'] = '{}_s{}.snap'.format(s['name'], s['sequence'])
225+ if s['is_oem']:
226+ oem = s['fname']
227+ else:
228+ installs += ['--install', s['fname']]
229+
230+ if core is not None:
231+ base_snaps.remove(core)
232+ if oem is None:
233+ log.error('OEM snap is required to be specified in the Product, but none found')
234+ sys.exit(1)
235+ # log.info('base_snaps: %r', base_snaps)
236+ log.info('oem: %s, installs: %r', oem, installs)
237+
238+ # downlaod snaps - TODO: parallelize this
239+ for s in base_snaps:
240+ download_snap(s['url'], s['fname'])
241+
242+ # TODO: consider writing the udf cmd or the list of snaps/seqs out next to the image
243+
244+ sys_image_release = 'rolling' # XXX: ditch when all-snaps
245+ sys_image_chan = 'edge' # XXX: ditch when all-snaps
246+ udf_args = ['sudo', 'ubuntu-device-flash', 'core', sys_image_release,
247+ '--developer-mode',
248+ '--device', 'raspi2_armhf',
249+ '-o', image_filename,
250+ '--channel', sys_image_chan,
251+ '--oem', oem
252+ ] + installs
253+
254+ log.info('Composing image with: %s', ' '.join(udf_args))
255+ log.warn('Note: you may be required to enter your password for sudo. This is a result of the '
256+ 'current ubuntu-device-flash requiring root access.')
257+ subprocess.check_call(udf_args)

Subscribers

People subscribed via source and target branches

to all changes: