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
=== modified file 'Makefile'
--- Makefile 2015-10-30 21:14:35 +0000
+++ Makefile 2015-11-23 20:28:24 +0000
@@ -19,7 +19,7 @@
1919
20raw-lint:20raw-lint:
21 env/bin/flake8 *.py scripts/*.py --max-line-length=9921 env/bin/flake8 *.py scripts/*.py --max-line-length=99
22 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_input22 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
23 env/bin/flake8 rpi2-sample-provkit/test_tarball_content/test --max-line-length=9923 env/bin/flake8 rpi2-sample-provkit/test_tarball_content/test --max-line-length=99
2424
25raw-test:25raw-test:
2626
=== added file 'rpi2-sample-provkit/provision2'
--- rpi2-sample-provkit/provision2 1970-01-01 00:00:00 +0000
+++ rpi2-sample-provkit/provision2 2015-11-23 20:28:24 +0000
@@ -0,0 +1,114 @@
1#!/usr/bin/python -u
2
3from __future__ import print_function
4
5import argparse
6import codecs
7import json
8import os
9import subprocess
10import time
11
12import utils
13
14MAX_RETRIES = 5
15
16STEPS = """
171. Write the image into the Raspberry Pi 2 SDCard (remote flashing of RPi2 is
18 still in-development), in another shell/window:
19
20 sudo dd if={} of=SDCARD_DEVICE bs=32M
21
22 where SDCARD_DEVICE
23 is the device in your system that holds the SDCard (like '/dev/sdb',
24 but be SURE that you're choosing the right device, otherwise you may
25 overwrite preexisting data on a device in your machine).
26
272. Insert the card into the Raspberry Pi 2, and reboot it.
28"""
29
30log = utils.get_prov_kit_logger()
31
32
33def run_sshrobot(sshopts, cmd):
34 """Run a command using sshrobot helper."""
35 sshrobot = os.path.join(os.path.dirname(__file__), 'sshrobot')
36 invoke = [sshrobot] + sshopts.ssh + [cmd]
37 p = subprocess.Popen(invoke, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
38 buf = ""
39 while True:
40 r = p.poll()
41 if r is not None:
42 out = (buf + p.stdout.read().decode("utf8")).strip()
43 if out:
44 print(out)
45 return r
46 ch = p.stdout.read(1).decode("utf8")
47 buf += ch
48 if buf.endswith(" password: "):
49 break
50 p.stdin.write(sshopts.password + "\n")
51 p.stdin.flush()
52 r = p.wait()
53 out = p.stdout.read().decode("utf8").strip()
54 if out:
55 print(out)
56 return r
57
58
59def wait_device(sshopts):
60 """Wait for device to answer SSH."""
61 ssh_identity = os.getenv('SSH_IDENTITY', '~/.ssh/id_rsa.pub')
62 pubpath = os.path.expanduser(ssh_identity)
63 with open(pubpath, "rb") as fh:
64 pubcontent = fh.read()
65 cmd = "echo '" + pubcontent + b"' >> ~/.ssh/authorized_keys"
66 for attempt_number in range(1, MAX_RETRIES + 1):
67 res = run_sshrobot(sshopts, cmd)
68 if res == 0:
69 return
70 if attempt_number == MAX_RETRIES:
71 break
72 log.info("device still not answering, waiting 10s and retrying {}/{}..."
73 .format(attempt_number, MAX_RETRIES-1))
74 time.sleep(10)
75
76 # we never reached the raspi in all attempts
77 log.error("device never answered, quitting!")
78 log.error("Please be sure that the Raspberry Pi 2 is in a functional state and retry.")
79 exit(1)
80
81
82def main(args):
83 """Main entry point."""
84 sshopts = utils.device_sshopts()
85 # get the test opportunity content
86 with codecs.open(args.test_opp, 'rt', encoding='utf8') as fh:
87 test_opp = json.load(fh)
88
89 log.info("Starting to provision the Raspberry Pi 2 (@ {address}:{port}),"
90 " for the test {spec_name}".format(address=sshopts.ip, port=sshopts.port, **test_opp))
91
92 # TODO: test_opp needs base_channel, base_snaps, architecture, base_image_id
93 arch = test_opp['platform'] # XXX: s/platform/architecture
94 release = test_opp['release']
95 channel = test_opp['base_channel']
96 base_snaps = test_opp['base_snaps']
97
98 image_filename = 'gm_{}_{}.img'.format(channel, test_opp['base_image_id'])
99 utils.compose_image(arch, release, channel, base_snaps, image_filename)
100
101 print(STEPS.format(image_filename, **test_opp))
102 print("Please press ENTER when all steps are done")
103 raw_input()
104
105 # wait for ssh to actually work
106 wait_device(sshopts)
107 log.info("Done.")
108
109
110if __name__ == "__main__":
111 parser = argparse.ArgumentParser()
112 parser.add_argument("test_opp")
113 args = parser.parse_args()
114 main(args)
0115
=== modified file 'rpi2-sample-provkit/setup'
--- rpi2-sample-provkit/setup 2015-10-30 21:14:35 +0000
+++ rpi2-sample-provkit/setup 2015-11-23 20:28:24 +0000
@@ -60,6 +60,7 @@
6060
61 # try to update in the running image61 # try to update in the running image
62 for snap_name, snap_revno in to_update:62 for snap_name, snap_revno in to_update:
63 # XXX: use new CPI and download function in utils.py
63 log.info('requesting package info for snap: %s, headers: %r', snap_name, headers)64 log.info('requesting package info for snap: %s, headers: %r', snap_name, headers)
64 res = requests.get(PACKAGE_URL.format(snap_name), headers=headers)65 res = requests.get(PACKAGE_URL.format(snap_name), headers=headers)
65 if res.status_code != 200:66 if res.status_code != 200:
6667
=== modified file 'rpi2-sample-provkit/utils.py'
--- rpi2-sample-provkit/utils.py 2015-10-30 21:14:35 +0000
+++ rpi2-sample-provkit/utils.py 2015-11-23 20:28:24 +0000
@@ -1,15 +1,24 @@
1import logging1import logging
2import os2import os
3import sys3import sys
4import requests
5import subprocess
6
7
8PACKAGE_URL = "https://search.apps.ubuntu.com/api/v1/package/{}"
49
510
6def get_prov_kit_logger():11def get_prov_kit_logger():
7 """Configure logging and return the provkit logger."""12 """Configure logging and return the provkit logger."""
8 logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s: %(message)s",13 logging.basicConfig(format="%(asctime)s %(levelname)s %(name)s: %(message)s",
9 stream=sys.stderr, level=logging.INFO)14 stream=sys.stderr, level=logging.INFO)
15 logging.getLogger('requests.packages.urllib3.connectionpool').setLevel(logging.ERROR)
10 return logging.getLogger("provkit.{}".format(os.path.basename(sys.argv[0])))16 return logging.getLogger("provkit.{}".format(os.path.basename(sys.argv[0])))
1117
1218
19log = get_prov_kit_logger()
20
21
13def device_sshopts():22def device_sshopts():
14 opts = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']23 opts = ['-o', 'StrictHostKeyChecking=no', '-o', 'UserKnownHostsFile=/dev/null']
1524
@@ -21,3 +30,84 @@
21 ssh = opts + ['-q', '-p', str(port), target]30 ssh = opts + ['-q', '-p', str(port), target]
22 scp = opts + ['-q', '-P', str(port)]31 scp = opts + ['-q', '-P', str(port)]
23 return sshopts32 return sshopts
33
34
35def get_snap_info(arch, release, channel, snap_name, snap_seq, bail_on_seq_mismatch=True):
36 """ Fetch package info from Click Package Index (CPI) and return the download URL
37 and a boolean is_oem if this is an OEM type package. """
38
39 headers = {
40 'X-Ubuntu-Architecture': arch,
41 'X-Ubuntu-Release': release
42 }
43
44 url = PACKAGE_URL.format(snap_name)
45 log.info('Requesting package info for snap: %s, url: %s, headers: %r', snap_name, url, headers)
46
47 res = requests.get(url, headers=headers)
48 if res.status_code != 200:
49 log.error('Cannot find package info for snap: %s, status_code: %d',
50 snap_name, res.status_code)
51 sys.exit(1)
52 info = res.json()
53 if info['sequence'] != snap_seq:
54 log.error("cannot get hold of {} sequence {}, it's not the current latest: {}"
55 .format(snap_name, snap_seq, info['sequence']))
56 sys.exit(1)
57
58 url = info['anon_download_url']
59 is_oem = info['content'] == 'oem'
60 return url, is_oem
61
62
63def download_snap(url, output_filename):
64 # TODO: consider keeping a cache of downloads
65 log.info("Downloading: {}".format(url))
66 subprocess.check_call(['curl', '-o', output_filename, url])
67
68
69def compose_image(arch, release, channel, base_snaps, image_filename):
70 """ Gather snap info, downlaod snaps, and use ubuntu-device-flash to compose an image"""
71 # gather snap info
72 oem = None
73 installs = []
74 for s in base_snaps:
75 if s['name'] == 'ubuntu-core': # XXX: remove once we have all-snaps
76 log.warn('Skipping ubuntu-core snap because we are still in hybrid system-image world')
77 core = s
78 continue
79 s['url'], s['is_oem'] = get_snap_info(arch, release, channel, s['name'], s['sequence'])
80 s['fname'] = '{}_s{}.snap'.format(s['name'], s['sequence'])
81 if s['is_oem']:
82 oem = s['fname']
83 else:
84 installs += ['--install', s['fname']]
85
86 if core is not None:
87 base_snaps.remove(core)
88 if oem is None:
89 log.error('OEM snap is required to be specified in the Product, but none found')
90 sys.exit(1)
91 # log.info('base_snaps: %r', base_snaps)
92 log.info('oem: %s, installs: %r', oem, installs)
93
94 # downlaod snaps - TODO: parallelize this
95 for s in base_snaps:
96 download_snap(s['url'], s['fname'])
97
98 # TODO: consider writing the udf cmd or the list of snaps/seqs out next to the image
99
100 sys_image_release = 'rolling' # XXX: ditch when all-snaps
101 sys_image_chan = 'edge' # XXX: ditch when all-snaps
102 udf_args = ['sudo', 'ubuntu-device-flash', 'core', sys_image_release,
103 '--developer-mode',
104 '--device', 'raspi2_armhf',
105 '-o', image_filename,
106 '--channel', sys_image_chan,
107 '--oem', oem
108 ] + installs
109
110 log.info('Composing image with: %s', ' '.join(udf_args))
111 log.warn('Note: you may be required to enter your password for sudo. This is a result of the '
112 'current ubuntu-device-flash requiring root access.')
113 subprocess.check_call(udf_args)

Subscribers

People subscribed via source and target branches

to all changes: